throughline 0.12.7 → 0.12.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,13 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.12.8] — 2026-10-04
|
|
14
|
+
|
|
15
|
+
### 変更
|
|
16
|
+
|
|
17
|
+
- runtime errorの送信を有効にした端末は、未受領の記録が無くても、受け口へ最後に届いた版と今の版が違う時に、`runtime_errors`と`resolutions`が空のreportを1回送る([ADR 0030](docs/adr/0030-report-installed-version-once.md))。reportは`installed_version`を持つので、受け口は記録が出ていない端末の版も分かる。届いた後は、版が変わるまで送らない。reportの形、署名、送る時機と間隔は変えない。送信を有効にしていない端末(既定)と、収集が無効な端末は、今までどおり何も送らない。
|
|
18
|
+
- 0.12.7以前は、未受領の記録が無ければ通信しなかった。更新した端末で何も起きなければ、受け口は古い版を持ち続けていた。
|
|
19
|
+
|
|
13
20
|
## [0.12.7] — 2026-10-04
|
|
14
21
|
|
|
15
22
|
### 修正
|
|
@@ -1614,7 +1621,8 @@ two attempts, instrument first instead of patching again.
|
|
|
1614
1621
|
|
|
1615
1622
|
---
|
|
1616
1623
|
|
|
1617
|
-
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.12.
|
|
1624
|
+
[Unreleased]: https://github.com/kitepon/Throughline/compare/v0.12.8...HEAD
|
|
1625
|
+
[0.12.8]: https://github.com/kitepon/Throughline/compare/v0.12.7...v0.12.8
|
|
1618
1626
|
[0.12.7]: https://github.com/kitepon/Throughline/compare/v0.12.6...v0.12.7
|
|
1619
1627
|
[0.12.6]: https://github.com/kitepon/Throughline/compare/v0.12.5...v0.12.6
|
|
1620
1628
|
[0.12.5]: https://github.com/kitepon/Throughline/compare/v0.12.4...v0.12.5
|
package/README.md
CHANGED
|
@@ -890,7 +890,7 @@ once with `npm install --global throughline@latest`, then run
|
|
|
890
890
|
| `throughline runtime-errors compact --json` | Remove only acknowledged, resolved aggregates after retention; open or unacknowledged records remain |
|
|
891
891
|
| `throughline runtime-errors report-enable --credential-file <path> --json` | Opt in to sending aggregates to the receiver named in that credential file; sending is disabled by default |
|
|
892
892
|
| `throughline runtime-errors report-disable --json` | Stop sending aggregates |
|
|
893
|
-
| `throughline runtime-errors report --json` | Send unacknowledged aggregates once now; exits 0 only for `sent` or `nothing_pending` |
|
|
893
|
+
| `throughline runtime-errors report --json` | Send unacknowledged aggregates once now (or an empty report when the receiver has not seen this version yet); exits 0 only for `sent` or `nothing_pending` |
|
|
894
894
|
| `throughline runtime-errors report-status --json` | Read the last attempt time and fixed result code without exposing the receiver, credential, or paths |
|
|
895
895
|
| `throughline handoff-preview --session <id>` | Print a Codex-facing `throughline_handoff` JSON projection |
|
|
896
896
|
| `throughline handoff-context (--session <id> \| --project <path>) --json` | Print the SessionStart inheritance context without moving memory rows. Project mode selects the newest captured session with dialogue, supports `--disclosure silent`, and returns `empty` when no dialogue exists. Session mode may add a project-bound supplement inside the same 9,500-character budget |
|
|
@@ -948,8 +948,10 @@ throughline runtime-errors report-status --json
|
|
|
948
948
|
```
|
|
949
949
|
|
|
950
950
|
Once enabled, the Claude and Codex hook entry points start a detached sender at
|
|
951
|
-
most once per hour
|
|
952
|
-
|
|
951
|
+
most once per hour; the hooks do not wait for it. It contacts the receiver when
|
|
952
|
+
unacknowledged records exist, or once after the installed version changed (a
|
|
953
|
+
report with empty `runtime_errors` and `resolutions`, so the receiver learns the
|
|
954
|
+
version; see [ADR 0030](docs/adr/0030-report-installed-version-once.md)). The body carries the public snapshot fields
|
|
953
955
|
only (fixed error code, template, count, timestamps, version, resolutions). The
|
|
954
956
|
secret is never transmitted: the body is signed with
|
|
955
957
|
`HMAC-SHA256(secret, ts + "\n" + SHA-256(body))`, redirects are not followed,
|
|
@@ -38,7 +38,7 @@ BugHub は、端末に入る CLI 製品が自分の分だけを送る受け口
|
|
|
38
38
|
7. 送る時機は hook 入口。`process-turn`・`session-start`・`prompt-submit`・`codex-hook` の開始時に、
|
|
39
39
|
送信が有効で、次に試してよい時刻を過ぎていれば、切り離した子 process を1つ起こす。
|
|
40
40
|
hook の終了は待たせない。子 process が store lock の下で時刻を先に書くので、同時に起きても送るのは1つ。
|
|
41
|
-
|
|
41
|
+
未受領の記録が無ければ通信しない(2026-10-04 [ADR 0030](0030-report-installed-version-once.md)で、受け口へ届いた版と今の版が違う時だけ、空の report を1回送ることにした)。間隔は1時間。credential や report の形で断られた時は24時間空ける。
|
|
42
42
|
`throughline runtime-errors report --json` は間隔を待たずに1回送る。
|
|
43
43
|
8. 送信の状態(最後に試した時刻・結果の固定 code・次の時刻)は `runtime-errors.report.state.json` に持ち、
|
|
44
44
|
`runtime-errors report-status --json` で読む。宛先・credential・path は出さない。
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# ADR 0030: 未受領の記録が無くても、受け口へ届いた版と今の版が違う時は、空の report を1回送る
|
|
2
|
+
|
|
3
|
+
日付: 2026-10-04
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
ADR 0025 Decision 7 は、未受領の記録(runtime error と解決記録)が無ければ通信しないと決めている。
|
|
8
|
+
report は毎回 `installed_version` を持つが、記録が無い端末からは report が出ない。
|
|
9
|
+
|
|
10
|
+
BugHub は 2026-10-04 から、工場の report を送らない端末について、製品の report の `installed_version` と
|
|
11
|
+
`observed_at` を端末の導入版として表示する。受け口は `runtime_errors` と `resolutions` が空の report も受ける
|
|
12
|
+
(BugHub の担当が確認。同じ端末×製品は1分に1回まで)。
|
|
13
|
+
|
|
14
|
+
BellTeam コンテナは工場の report を送らない。0.12.7 へ更新して未解決が0件になった後は、
|
|
15
|
+
次に runtime error か解決記録が出るまで report が出ず、BugHub の導入版は「未確認」のままになる。
|
|
16
|
+
更新した端末で何も起きなければ、受け口は古い版を持ち続ける。
|
|
17
|
+
|
|
18
|
+
オーナーは 2026-10-04 に、エラーが無い時にも版を知らせる report を送ってよいか、送るなら
|
|
19
|
+
版が変わった時に1回だけか・1時間ごとかを選び、「版が変わった時に1回だけ送る」と決めた。
|
|
20
|
+
|
|
21
|
+
## Decision
|
|
22
|
+
|
|
23
|
+
1. 送信の状態(`runtime-errors.report.state.json`)に、受け口が受領した最後の report の版
|
|
24
|
+
`last_reported_version` を持つ。書くのは、report が `sent`(ADR 0025 Decision 6 の受領がそろった時)に
|
|
25
|
+
なった時だけ。記録を載せた report でも、空の report でも同じ。
|
|
26
|
+
2. 未受領の記録が無い時、`last_reported_version` が今の版と同じなら、今までどおり通信しない(`nothing_pending`)。
|
|
27
|
+
違う時(版を上げた・下げた後、この項目を持たない旧版の state)は、`runtime_errors` と `resolutions` が
|
|
28
|
+
空の report を1回送る。
|
|
29
|
+
3. 空の report の形は ADR 0025 と同じ。項目は `schema_version`・`report_id`・`product_id`・
|
|
30
|
+
`installed_version`・`observed_at`・空の `runtime_errors`・空の `resolutions`。wire の版は変えない。
|
|
31
|
+
署名、redirect を追わないこと、受領の確認も同じ。
|
|
32
|
+
4. 送るのは、今までどおり `report-enable` で明示して有効にし、収集も有効な端末だけ。
|
|
33
|
+
送信が無効な端末と収集が無効な端末は、版も送らない。
|
|
34
|
+
5. 送る時機と間隔は変えない(hook 入口、1時間、断られた時は ADR 0025 の間隔)。
|
|
35
|
+
断られた時と届いたか分からない時は `last_reported_version` を書かず、次の送信でもう一度送る。
|
|
36
|
+
6. `runtime-errors report --json` の結果は `sent`(`runtime_errors: 0`・`resolutions: 0`)。
|
|
37
|
+
`report-status` の項目は変えない。
|
|
38
|
+
|
|
39
|
+
## Consequences
|
|
40
|
+
|
|
41
|
+
- 送信を有効にした端末は、0.12.8 へ上げた後に1回、その後は版が変わるたびに1回、空の report を送る。
|
|
42
|
+
受け口には、端末がその版で動いた時刻が1つ届く。
|
|
43
|
+
- 版が同じ間は、記録が無ければ通信しない。端末が動いているかどうかは、この report からは分からない。
|
|
44
|
+
- 版を上げた後、次の送信の時刻(最長1時間)と、その後の最初の hook までは届かない。
|
|
45
|
+
急ぐ時は `runtime-errors report --json` を回す。
|
|
46
|
+
- 0.12.7 以前へ戻すと、旧版は state を書き直す時に `last_reported_version` を落とす。
|
|
47
|
+
再び上げた時に、もう1回送る。
|
|
48
|
+
- 既定の利用者(送信を有効にしていない端末)の動作は変わらない。
|
package/package.json
CHANGED
|
@@ -131,22 +131,30 @@ export async function reportRuntimeErrors(options = {}) {
|
|
|
131
131
|
|
|
132
132
|
// 記録が無効な端末でも次の時刻は書く。書かないと、hookのたびに送信processが起きる。
|
|
133
133
|
const outcome = isRuntimeErrorCollectionEnabled({ env })
|
|
134
|
-
? await sendPendingRecords({
|
|
134
|
+
? await sendPendingRecords({
|
|
135
|
+
env, nowMs, fetchImpl, storeOptions,
|
|
136
|
+
credentialFile: config.reporting.credential_file,
|
|
137
|
+
lastReportedVersion: claimed.last_reported_version,
|
|
138
|
+
})
|
|
135
139
|
: result('collection_disabled');
|
|
136
140
|
updateState(lockOptions, (state) => ({
|
|
137
141
|
...state,
|
|
138
142
|
last_result: outcome.status,
|
|
139
143
|
last_success_at: outcome.status === 'sent' ? new Date(nowMs).toISOString() : state.last_success_at,
|
|
144
|
+
last_reported_version: outcome.status === 'sent' ? PACKAGE_VERSION : state.last_reported_version,
|
|
140
145
|
next_attempt_at: new Date(nowMs + (LONG_BACKOFF_STATUSES.has(outcome.status)
|
|
141
146
|
? REPORT_REJECTED_BACKOFF_MS : REPORT_INTERVAL_MS)).toISOString(),
|
|
142
147
|
}));
|
|
143
148
|
return outcome;
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
async function sendPendingRecords({ env, nowMs, fetchImpl, storeOptions, credentialFile }) {
|
|
151
|
+
async function sendPendingRecords({ env, nowMs, fetchImpl, storeOptions, credentialFile, lastReportedVersion }) {
|
|
147
152
|
const acknowledged = readRuntimeErrorSnapshot({ ...storeOptions, limit: 1 }).cursor.acknowledged_through;
|
|
148
153
|
const snapshot = readRuntimeErrorSnapshot({ ...storeOptions, afterCursor: acknowledged });
|
|
149
|
-
|
|
154
|
+
// 未受領の記録が無くても、受け口へ最後に届いた版と今の版が違う時は、記録が空のreportを1回送る
|
|
155
|
+
// (ADR 0030)。受け口は installed_version を端末の導入版として持つ。届いた後は、版が変わるまで送らない。
|
|
156
|
+
if (snapshot.runtime_errors.length === 0 && snapshot.resolutions.length === 0 &&
|
|
157
|
+
lastReportedVersion === PACKAGE_VERSION) {
|
|
150
158
|
return result('nothing_pending', { acknowledged_through: acknowledged });
|
|
151
159
|
}
|
|
152
160
|
|
|
@@ -321,6 +329,7 @@ function emptyState() {
|
|
|
321
329
|
last_result: null,
|
|
322
330
|
last_success_at: null,
|
|
323
331
|
next_attempt_at: null,
|
|
332
|
+
last_reported_version: null,
|
|
324
333
|
};
|
|
325
334
|
}
|
|
326
335
|
|
|
@@ -332,6 +341,9 @@ function normalizeState(value) {
|
|
|
332
341
|
if (isCanonicalTimestamp(value[key])) state[key] = value[key];
|
|
333
342
|
}
|
|
334
343
|
if (RESULT_STATUSES.has(value.last_result)) state.last_result = value.last_result;
|
|
344
|
+
if (typeof value.last_reported_version === 'string' && /^[0-9A-Za-z.+-]{1,64}$/.test(value.last_reported_version)) {
|
|
345
|
+
state.last_reported_version = value.last_reported_version;
|
|
346
|
+
}
|
|
335
347
|
return state;
|
|
336
348
|
}
|
|
337
349
|
|
|
@@ -393,6 +393,76 @@ test('runtime error report: background runs respect the interval, explicit runs
|
|
|
393
393
|
}
|
|
394
394
|
});
|
|
395
395
|
|
|
396
|
+
test('runtime error report: with nothing pending, the installed version is reported once and again only after it changes', async () => {
|
|
397
|
+
const { root, env } = createEnvironment();
|
|
398
|
+
const installed = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
399
|
+
let reply = (entry) => accept(entry);
|
|
400
|
+
const receiver = await startReceiver((entry) => reply(entry));
|
|
401
|
+
const statePath = defaultRuntimeErrorReportStatePath(env);
|
|
402
|
+
const readState = () => JSON.parse(readFileSync(statePath, 'utf8'));
|
|
403
|
+
try {
|
|
404
|
+
setRuntimeErrorCollectionEnabled(true, { env });
|
|
405
|
+
setRuntimeErrorReportingEnabled(true, { env, credentialFile: writeCredential(root, receiver.url) });
|
|
406
|
+
|
|
407
|
+
// 断られた時は届いた版として覚えない。次の送信でもう一度送る。
|
|
408
|
+
reply = () => ({ status: 429, body: { error: 'rate_limited' } });
|
|
409
|
+
assert.equal((await reportRuntimeErrors({ env, now: NOW })).status, 'rate_limited');
|
|
410
|
+
assert.equal(readState().last_reported_version, null);
|
|
411
|
+
|
|
412
|
+
// 記録が1件も無い端末(0.12.7以前から上げた直後のstateも同じ): 版と時刻だけのreportを1回送る。
|
|
413
|
+
reply = (entry) => accept(entry);
|
|
414
|
+
const first = await reportRuntimeErrors({ env, now: NOW + 1000 });
|
|
415
|
+
assert.equal(first.status, 'sent');
|
|
416
|
+
assert.equal(first.runtime_errors, 0);
|
|
417
|
+
assert.equal(first.resolutions, 0);
|
|
418
|
+
assert.equal(receiver.requests.length, 2);
|
|
419
|
+
const sent = receiver.requests[1];
|
|
420
|
+
assert.equal(sent.signatureValid, true);
|
|
421
|
+
assert.deepEqual(sent.report, {
|
|
422
|
+
schema_version: '1.0',
|
|
423
|
+
report_id: first.report_id,
|
|
424
|
+
product_id: 'throughline',
|
|
425
|
+
installed_version: installed,
|
|
426
|
+
observed_at: new Date(NOW + 1000).toISOString(),
|
|
427
|
+
runtime_errors: [],
|
|
428
|
+
resolutions: [],
|
|
429
|
+
});
|
|
430
|
+
assert.equal(readState().last_reported_version, installed);
|
|
431
|
+
assert.equal(readRuntimeErrorSnapshot({ env }).cursor.acknowledged_through, 0);
|
|
432
|
+
|
|
433
|
+
// 同じ版のままなら、記録が無い間は通信しない。
|
|
434
|
+
assert.equal((await reportRuntimeErrors({ env, now: NOW + 2000 })).status, 'nothing_pending');
|
|
435
|
+
assert.equal((await reportRuntimeErrors({ env, now: NOW + 2 * REPORT_INTERVAL_MS, background: true })).status, 'nothing_pending');
|
|
436
|
+
assert.equal(receiver.requests.length, 2);
|
|
437
|
+
|
|
438
|
+
// 記録を送ったreportも、その版を届けたことになる。
|
|
439
|
+
observeRuntimeError({ code: 'HOOK_CODEX_FAILED' }, { env });
|
|
440
|
+
assert.equal((await reportRuntimeErrors({ env, now: NOW + 3 * REPORT_INTERVAL_MS })).status, 'sent');
|
|
441
|
+
assert.equal((await reportRuntimeErrors({ env, now: NOW + 3 * REPORT_INTERVAL_MS + 1000 })).status, 'nothing_pending');
|
|
442
|
+
assert.equal(receiver.requests.length, 3);
|
|
443
|
+
|
|
444
|
+
// 版が変わった(最後に届いた版が今の版と違う)時は、もう1回だけ送る。
|
|
445
|
+
writeFileSync(statePath, JSON.stringify({ ...readState(), last_reported_version: '0.0.1' }));
|
|
446
|
+
const afterUpdate = await reportRuntimeErrors({ env, now: NOW + 4 * REPORT_INTERVAL_MS });
|
|
447
|
+
assert.equal(afterUpdate.status, 'sent');
|
|
448
|
+
assert.deepEqual(receiver.requests[3].report.runtime_errors, []);
|
|
449
|
+
assert.equal(receiver.requests[3].report.installed_version, installed);
|
|
450
|
+
assert.equal((await reportRuntimeErrors({ env, now: NOW + 4 * REPORT_INTERVAL_MS + 1000 })).status, 'nothing_pending');
|
|
451
|
+
assert.equal(receiver.requests.length, 4);
|
|
452
|
+
|
|
453
|
+
// 記録を無効にした端末と、送信を無効にした端末は、版も送らない。
|
|
454
|
+
writeFileSync(statePath, JSON.stringify({ ...readState(), last_reported_version: '0.0.1' }));
|
|
455
|
+
setRuntimeErrorCollectionEnabled(false, { env });
|
|
456
|
+
assert.equal((await reportRuntimeErrors({ env, now: NOW + 5 * REPORT_INTERVAL_MS })).status, 'collection_disabled');
|
|
457
|
+
setRuntimeErrorCollectionEnabled(true, { env });
|
|
458
|
+
setRuntimeErrorReportingEnabled(false, { env });
|
|
459
|
+
assert.equal((await reportRuntimeErrors({ env, now: NOW + 6 * REPORT_INTERVAL_MS })).status, 'reporting_disabled');
|
|
460
|
+
assert.equal(receiver.requests.length, 4);
|
|
461
|
+
} finally {
|
|
462
|
+
await receiver.close();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
396
466
|
test('runtime error report: an unsafe or malformed credential file is never used', { skip: process.platform === 'win32' }, async () => {
|
|
397
467
|
const { root, env } = createEnvironment();
|
|
398
468
|
let calls = 0;
|