diff --git a/CHANGELOG.md b/CHANGELOG.md index beb756d7..59f72c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.73.0 — 2026-10-03 + +### runtime errorの収集: 対応しないOSは `unsupported` と答える + +- Windowsでは、`lattice runtime-errors snapshot`/`diagnostics` が `collection: "disabled"` を返していた。 + 設定が有効でも同じ答えで、「設定で止めている」のか「このOSでは収集できない」のかを区別できなかった。 + Windowsでは `collection: "unsupported"` を返す。`status` は `not_applicable`、`cursor` は0、記録は空、 + `diagnostics` のキーは今までと同じ。 +- 収集そのものは変わらない。Windowsではstoreの所有者と権限をPOSIXの形で確かめられないので、記録を作らない。 +- 受け側の前提: dotagents `49709de` 以降(`unsupported` を受け入れる)。 + ## 0.72.1 — 2026-10-03 ### 修正 diff --git a/docs/01_integration-package.md b/docs/01_integration-package.md index c9a45c57..2f65137e 100644 --- a/docs/01_integration-package.md +++ b/docs/01_integration-package.md @@ -225,6 +225,10 @@ native Windowsでは`HOST_PLATFORM_UNSUPPORTED`を返し、設定やstateへ書 reporting(BugHub送信)はdotagents adapter所有で本storeは外部送信しない(collection/reporting分離)。 固定catalog 5 code・fingerprint集約・cursor/ack・resolved+ack済み30日compact・POSIX owner-only検査で fail closed。正典は`src/runtime-errors.mjs` + - `diagnostics.collection`は`enabled`・`disabled`・`unsupported`の3値。`unsupported`は「このOSでは収集に + 対応しない」という製品の答えで、Windowsが返す(storeの所有者と権限をPOSIXの形で確かめられない)。 + 設定が有効でも記録は作らない。`status`・`cursor`・配列・`diagnostics`のキーは`disabled`の時と同じ形 + (`not_applicable`・すべて0・空)。受け側の前提はdotagents `49709de`以降。 - 各記録は任意の`safe_context`を持つ: `command_kind`(落ちたCLIの面。`run.list`・`todo.start`等、 Latticeが持つ一覧の語だけ。無ければ`other`)、`error_kind`(例外の種類。一覧に無ければ`other`)、 `cause_code`(Nodeが付けるerror code。無ければ`none`)。付ける時は3つを必ずそろえる。 diff --git a/package-lock.json b/package-lock.json index b8b2bb32..d05f0e34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@quolu/lattice", - "version": "0.72.1", + "version": "0.73.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@quolu/lattice", - "version": "0.72.1", + "version": "0.73.0", "license": "PolyForm-Noncommercial-1.0.0", "dependencies": { "@clack/prompts": "^1.3.0", diff --git a/package.json b/package.json index 6f081be2..20858042 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@quolu/lattice", - "version": "0.72.1", + "version": "0.73.0", "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution", "author": { "name": "Quo / クオ at kitepon.dev", diff --git a/src/runtime-errors.mjs b/src/runtime-errors.mjs index 9b538bfc..770c7539 100644 --- a/src/runtime-errors.mjs +++ b/src/runtime-errors.mjs @@ -76,7 +76,14 @@ function canonicalReporting(value) { return !value.enabled || (value.endpoint !== undefined && value.credential_file !== undefined); } +// このOSで収集に対応するか。Windowsはstoreの所有者と権限をPOSIXの形で確かめられない(`ensureSafeDir`)ので +// 対応しない。設定が有効でも記録は作らず、工場へは`disabled`でなく`unsupported`と答える——設定は有効なのに +// 製品が黙って無効と答えると、受け側は故障と区別できない。 +const collectionSupported = (options = {}) => (options.platform ?? hostPlatform()) !== 'win32'; +const inactiveCollection = (options = {}) => (collectionSupported(options) ? 'disabled' : 'unsupported'); + function collectionEnabled(options = {}) { + if (!collectionSupported(options)) return false; const env = options.env ?? process.env; try { const path = options.configPath ?? defaultFactoryReporterConfigPath(env); @@ -264,7 +271,7 @@ function snapshot(options = {}) { runtime_errors: rows.filter((record) => record.status === 'open').map(({ product_version, error_code, component, status, severity, fingerprint, message_template, count, first_seen, last_seen, state_schema_version, safe_context }) => ({ product_version, error_code, component, status, severity, fingerprint, message_template, occurrence_count: count, first_seen, last_seen, state_schema_version, ...(safe_context === undefined ? {} : { safe_context }) })), resolutions: rows.filter((record) => record.status === 'resolved').map(({ fingerprint, resolved_at, reason_code }) => ({ fingerprint, resolved_at, reason_code })), diagnostics: { - collection: enabled ? 'enabled' : 'disabled', + collection: enabled ? 'enabled' : inactiveCollection(options), status: enabled ? 'ready' : 'not_applicable', total_count: store.records.length, pending_count: store.records.filter((record) => record.sequence > store.acknowledged_through).length, @@ -279,7 +286,7 @@ export function runtimeErrorsSnapshot(afterCursor = 0, limit = MAX_RECORDS, opti export function runtimeErrorsDiagnostics(options = {}) { if (!collectionEnabled(options)) { - return { schema: DIAGNOSTICS_SCHEMA, collection: 'disabled', status: 'not_applicable', total_count: 0, open_count: 0, pending_count: 0, high_watermark: 0, acknowledged_through: 0 }; + return { schema: DIAGNOSTICS_SCHEMA, collection: inactiveCollection(options), status: 'not_applicable', total_count: 0, open_count: 0, pending_count: 0, high_watermark: 0, acknowledged_through: 0 }; } try { const { path } = optionsFor(options); @@ -300,7 +307,7 @@ export function runtimeErrorsDiagnostics(options = {}) { } export function recordRuntimeError(code, options = {}) { - if (!collectionEnabled(options)) return { status: 'disabled' }; + if (!collectionEnabled(options)) return { status: inactiveCollection(options) }; const definition = definitions[code]; if (!definition) throw Error('unknown_runtime_code'); const { path } = optionsFor(options); diff --git a/test/integration/hold-resume.integration.mjs b/test/integration/hold-resume.integration.mjs index 9566662b..d749b60c 100644 --- a/test/integration/hold-resume.integration.mjs +++ b/test/integration/hold-resume.integration.mjs @@ -67,8 +67,11 @@ test('競合群の後継を先に起動し、無関係workerの旧epoch成果を await writeFile(path.join(repoRoot, 'test', `${symbol}.test.mjs`), unitTest(symbol)); } // T2が宣言外のsrc/alpha.mjs——T1のscope——へも書く。T3は無関係なのでcontinue_setへ落ちる。 + // `hold_ms`はworkerが書いた後に走り続ける時間で、holdはこの窓の中で届かなければならない。10秒では、 + // 混んだ機械で窓が先に閉じ、holdが`worker processを止められない`(ESRCH)で断られる。30秒は、2コアに + // CPU負荷16本を重ねた状態で通ることを確かめた値。 await writeFile(path.join(repoRoot, 'adapter-config.json'), - `${JSON.stringify({ mode: 'deterministic', hold_ms: 10_000, + `${JSON.stringify({ mode: 'deterministic', hold_ms: 30_000, extra_writes_by_todo: { T2: ['src/alpha.mjs'] } })}\n`); const git = (...args) => ok(invoke('git', ['-c', 'user.email=a@example.invalid', diff --git a/test/runtime-errors-platform.test.mjs b/test/runtime-errors-platform.test.mjs new file mode 100644 index 00000000..eeaf43a6 --- /dev/null +++ b/test/runtime-errors-platform.test.mjs @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; + +// `runtime-errors.test.mjs`はWindowsで全件skipする。この試験はどのOSでも走り、そのOSで実際に +// CLIが返す答えを確かめる——Windowsは`unsupported`、ほかは設定に従う。 +const execFileAsync = promisify(execFile); +const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'lattice.mjs'); +const windows = process.platform === 'win32'; +const VALID_CONFIG = { + schema_version: '1.0', + host: { id: 'test-host', profile: windows ? 'windows-native' : 'linux' }, + collection: { enabled: true }, + reporting: { enabled: false }, +}; + +async function runJson(args, env) { + const { stdout } = await execFileAsync(process.execPath, [cliPath, 'runtime-errors', ...args, '--json'], { env }); + return JSON.parse(stdout.trim()); +} + +test('CLIは収集に対応しないOSでunsupported、対応するOSでは設定どおりに答える', async () => { + const root = await mkdtemp(path.join(os.tmpdir(), 'lattice-rterr-platform-')); + try { + const env = { ...process.env, HOME: root, USERPROFILE: root, + XDG_CONFIG_HOME: path.join(root, 'xdg-config'), XDG_STATE_HOME: path.join(root, 'xdg-state') }; + + // 設定が無い時: 対応するOSは`disabled`、対応しないOSは`unsupported`。 + const inactive = windows ? 'unsupported' : 'disabled'; + const bare = await runJson(['snapshot'], env); + assert.deepEqual(bare.diagnostics, + { collection: inactive, status: 'not_applicable', total_count: 0, pending_count: 0, truncated: false }); + assert.equal((await runJson(['diagnostics'], env)).collection, inactive); + + // 設定が有効な時: 対応しないOSは、それでも`unsupported`のまま記録を作らない。 + await mkdir(path.join(root, 'xdg-config', 'dotagents'), { recursive: true }); + await writeFile(path.join(root, 'xdg-config', 'dotagents', 'factory-reporter.json'), JSON.stringify(VALID_CONFIG)); + const configured = await runJson(['snapshot'], env); + assert.equal(configured.diagnostics.collection, windows ? 'unsupported' : 'enabled'); + assert.equal(configured.diagnostics.status, windows ? 'not_applicable' : 'ready'); + assert.deepEqual(configured.cursor, { high_watermark: 0, acknowledged_through: 0, next: 0 }); + assert.deepEqual(configured.runtime_errors, []); + assert.deepEqual(configured.resolutions, []); + assert.deepEqual(Object.keys(configured.diagnostics), + ['collection', 'status', 'total_count', 'pending_count', 'truncated']); + if (windows) assert.equal(existsSync(path.join(root, 'xdg-state', 'lattice')), false); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/test/runtime-errors.test.mjs b/test/runtime-errors.test.mjs index 7c1b430e..2d0c3906 100644 --- a/test/runtime-errors.test.mjs +++ b/test/runtime-errors.test.mjs @@ -92,6 +92,29 @@ test('工場が名乗るhost profileはすべて収集を有効にし、未知 } }); +test('収集に対応しないOSでは、設定が有効でもunsupportedと答えstateへ触れない', async () => { + const workspace = await makeWorkspace(); + const options = { ...workspace.options, platform: 'win32' }; + try { + assert.deepEqual(recordRuntimeError('LATTICE.CLI_INTERNAL_FAILED', options), { status: 'unsupported' }); + const snapshot = runtimeErrorsSnapshot(0, 256, options); + // 工場(dotagents)が`unsupported`に課す形: status・cursor・配列・diagnosticsのキーは`disabled`の時と同じ。 + assert.deepEqual(snapshot.diagnostics, + { collection: 'unsupported', status: 'not_applicable', total_count: 0, pending_count: 0, truncated: false }); + assert.deepEqual(snapshot.cursor, { high_watermark: 0, acknowledged_through: 0, next: 0 }); + assert.deepEqual(snapshot.runtime_errors, []); + assert.deepEqual(snapshot.resolutions, []); + const diagnostics = runtimeErrorsDiagnostics(options); + assert.equal(diagnostics.collection, 'unsupported'); + assert.equal(diagnostics.status, 'not_applicable'); + assert.equal(existsSync(path.dirname(workspace.storePath)), false); + // 同じ設定でも、対応するOSでは有効と答える。 + assert.equal(runtimeErrorsDiagnostics({ ...workspace.options, platform: 'linux' }).collection, 'enabled'); + } finally { + await rm(workspace.root, { recursive: true, force: true }); + } +}); + test('同一原因はfingerprint集約でcount/last_seen/sequenceと発生版が進む', async () => { const workspace = await makeWorkspace(); try {