Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

### 修正
Expand Down
4 changes: 4 additions & 0 deletions docs/01_integration-package.md
Original file line number Diff line number Diff line change
Expand Up @@ -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つを必ずそろえる。
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
13 changes: 10 additions & 3 deletions src/runtime-errors.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion test/integration/hold-resume.integration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
56 changes: 56 additions & 0 deletions test/runtime-errors-platform.test.mjs
Original file line number Diff line number Diff line change
@@ -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 });
}
});
23 changes: 23 additions & 0 deletions test/runtime-errors.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading