instar 1.3.36 → 1.3.37

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.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * AgentTelegramLedger — append-only audit trail for the agent-to-agent Telegram comms
3
+ * primitive (spec MENTOR-LIVE-READINESS §Fix 2a "Round-trip audit ledger" + Codey's
4
+ * round-2 design point: separate sent + received JSONLs so Stage-B forensics can
5
+ * reconstruct the round trip from the ledger files alone, OR from Telegram chat history
6
+ * alone via the visible `corr=` field).
7
+ *
8
+ * Conventions:
9
+ * - Append-only JSONL (atomic at line granularity on POSIX for `<PIPE_BUF` writes —
10
+ * the same rationale the original mentor-outbox used; documented so a future reviewer
11
+ * doesn't "fix" it to atomicWriteFileSync, which is wrong for append).
12
+ * - **No secrets in any row** (round-2 adversarial F5 — and the SendAuditRow shape
13
+ * explicitly excludes the bot token; the ReceiveAuditRow only captures routing-level
14
+ * marker fields, never the body).
15
+ * - Default paths: `{stateDir}/a2a-sent.jsonl` + `{stateDir}/a2a-received.jsonl`. The
16
+ * caller may override (per-agent isolation, tests).
17
+ *
18
+ * I/O is the only side-effect; everything else is pure data.
19
+ */
20
+ import type { SendAuditRow, RouteDropReason } from './AgentTelegramComms.js';
21
+ /** Audit row written by the recipient wiring on every a2a event that was either routed
22
+ * to a role-handler or dropped. NO row is written when the message is `no-marker`
23
+ * (fall-through to normal user handling) — that would log every user message. */
24
+ export interface ReceiveAuditRow {
25
+ localTs: string;
26
+ direction: 'received';
27
+ decision: 'routed' | 'dropped';
28
+ /** When `dropped`, the reason code from the routing matrix (see RouteDropReason). */
29
+ dropReason?: RouteDropReason | 'agent-marker-malformed';
30
+ /** Parsed marker fields — present when the parse succeeded, undefined for malformed. */
31
+ fromAgent?: string;
32
+ toAgent?: string;
33
+ role?: string;
34
+ id?: string;
35
+ corr?: string;
36
+ ts?: number;
37
+ /** Telegram-side identifiers, when resolved. */
38
+ telegramFromBotId?: string;
39
+ telegramSenderChatId?: string;
40
+ topicId?: number;
41
+ /** First 200 chars of the raw inbound (post-marker-strip on route; raw on drop) so
42
+ * forensic readers can reconstruct what happened from the ledger alone. NEVER the
43
+ * full body for routed rows — the body went to the role-handler. */
44
+ rawPrefix?: string;
45
+ }
46
+ export interface AgentTelegramLedgerPaths {
47
+ sentPath: string;
48
+ receivedPath: string;
49
+ }
50
+ export declare function defaultLedgerPaths(stateDir: string): AgentTelegramLedgerPaths;
51
+ /**
52
+ * Append-only JSONL audit ledger. Both `appendSent` and `appendReceived` are synchronous
53
+ * + best-effort: they NEVER throw (an audit-write failure must not crash a tick). Writes
54
+ * use `fs.appendFileSync` because atomic-write-replace is wrong for append-only files
55
+ * (see the documented rationale in the module header).
56
+ */
57
+ export declare class AgentTelegramLedger {
58
+ private readonly sentPath;
59
+ private readonly receivedPath;
60
+ constructor(paths: AgentTelegramLedgerPaths);
61
+ appendSent(row: SendAuditRow): void;
62
+ appendReceived(row: ReceiveAuditRow): void;
63
+ private appendLine;
64
+ }
65
+ //# sourceMappingURL=AgentTelegramLedger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AgentTelegramLedger.d.ts","sourceRoot":"","sources":["../../src/messaging/AgentTelegramLedger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE7E;;kFAEkF;AAClF,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,UAAU,CAAC;IACtB,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC/B,qFAAqF;IACrF,UAAU,CAAC,EAAE,eAAe,GAAG,wBAAwB,CAAC;IACxD,wFAAwF;IACxF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;yEAEqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,wBAAwB,CAK7E;AAED;;;;;GAKG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,KAAK,EAAE,wBAAwB;IAK3C,UAAU,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI;IAInC,cAAc,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI;IAI1C,OAAO,CAAC,UAAU;CAYnB"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * AgentTelegramLedger — append-only audit trail for the agent-to-agent Telegram comms
3
+ * primitive (spec MENTOR-LIVE-READINESS §Fix 2a "Round-trip audit ledger" + Codey's
4
+ * round-2 design point: separate sent + received JSONLs so Stage-B forensics can
5
+ * reconstruct the round trip from the ledger files alone, OR from Telegram chat history
6
+ * alone via the visible `corr=` field).
7
+ *
8
+ * Conventions:
9
+ * - Append-only JSONL (atomic at line granularity on POSIX for `<PIPE_BUF` writes —
10
+ * the same rationale the original mentor-outbox used; documented so a future reviewer
11
+ * doesn't "fix" it to atomicWriteFileSync, which is wrong for append).
12
+ * - **No secrets in any row** (round-2 adversarial F5 — and the SendAuditRow shape
13
+ * explicitly excludes the bot token; the ReceiveAuditRow only captures routing-level
14
+ * marker fields, never the body).
15
+ * - Default paths: `{stateDir}/a2a-sent.jsonl` + `{stateDir}/a2a-received.jsonl`. The
16
+ * caller may override (per-agent isolation, tests).
17
+ *
18
+ * I/O is the only side-effect; everything else is pure data.
19
+ */
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ export function defaultLedgerPaths(stateDir) {
23
+ return {
24
+ sentPath: path.join(stateDir, 'a2a-sent.jsonl'),
25
+ receivedPath: path.join(stateDir, 'a2a-received.jsonl'),
26
+ };
27
+ }
28
+ /**
29
+ * Append-only JSONL audit ledger. Both `appendSent` and `appendReceived` are synchronous
30
+ * + best-effort: they NEVER throw (an audit-write failure must not crash a tick). Writes
31
+ * use `fs.appendFileSync` because atomic-write-replace is wrong for append-only files
32
+ * (see the documented rationale in the module header).
33
+ */
34
+ export class AgentTelegramLedger {
35
+ sentPath;
36
+ receivedPath;
37
+ constructor(paths) {
38
+ this.sentPath = paths.sentPath;
39
+ this.receivedPath = paths.receivedPath;
40
+ }
41
+ appendSent(row) {
42
+ this.appendLine(this.sentPath, row);
43
+ }
44
+ appendReceived(row) {
45
+ this.appendLine(this.receivedPath, row);
46
+ }
47
+ appendLine(target, row) {
48
+ try {
49
+ fs.mkdirSync(path.dirname(target), { recursive: true });
50
+ fs.appendFileSync(target, JSON.stringify(row) + '\n', 'utf-8');
51
+ }
52
+ catch (err) {
53
+ // Best-effort audit: an audit-write failure must not crash a tick. Log but swallow.
54
+ // (Spec §Fix 2a "every drop path writes an audit row" — silent drops make Stage-B
55
+ // forensics painful, but a CRASHED tick is worse. We accept this trade-off.)
56
+ // eslint-disable-next-line no-console
57
+ console.warn(`[a2a-ledger] append failed (non-fatal) at ${target}:`, err instanceof Error ? err.message : String(err));
58
+ }
59
+ }
60
+ }
61
+ //# sourceMappingURL=AgentTelegramLedger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AgentTelegramLedger.js","sourceRoot":"","sources":["../../src/messaging/AgentTelegramLedger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAkC7B,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC;QAC/C,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,oBAAoB,CAAC;KACxD,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,OAAO,mBAAmB;IACb,QAAQ,CAAS;IACjB,YAAY,CAAS;IAEtC,YAAY,KAA+B;QACzC,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;QAC/B,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;IACzC,CAAC;IAED,UAAU,CAAC,GAAiB;QAC1B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACtC,CAAC;IAED,cAAc,CAAC,GAAoB;QACjC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;IAC1C,CAAC;IAEO,UAAU,CAAC,MAAc,EAAE,GAAY;QAC7C,IAAI,CAAC;YACH,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACxD,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;QACjE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,oFAAoF;YACpF,kFAAkF;YAClF,6EAA6E;YAC7E,sCAAsC;YACtC,OAAO,CAAC,IAAI,CAAC,6CAA6C,MAAM,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACzH,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * ProcessedIdStore — small persistent set of recently-processed a2a marker `id`s, so
3
+ * Telegram retries / adapter restarts can't double-inject the same prompt (spec
4
+ * MENTOR-LIVE-READINESS §Fix 2a "Processed-id ledger" + Codey's round-2 design point).
5
+ *
6
+ * Bounded by `maxEntries` (default 10_000) AND `maxAgeMs` (default 30 days) — whichever
7
+ * evicts first. **Bounded eviction is the same surface adversarial F2 worried about**:
8
+ * an `id` that ages out is no longer "duplicate" and could be re-injected by a replayer
9
+ * who captured it. The marker's `ts` + skew-window (24h default) is the defense at the
10
+ * primitive layer; this store's eviction policy is set generously so legitimate retries
11
+ * within hours/days are still de-duped.
12
+ *
13
+ * Convention note: instar prefers SQLite for durable dedup ledgers (MessageProcessingLedger,
14
+ * CommitmentTracker). This store uses a small JSON file because the working set is small
15
+ * (per-mentee processed-ids over 30d), the file is rewritten atomically on every mark, and
16
+ * there's no concurrent-writer story to need WAL. A future bump to SQLite is a drop-in
17
+ * replacement at the class boundary.
18
+ */
19
+ export interface ProcessedIdStoreOptions {
20
+ /** Absolute path to the JSON file backing the store. */
21
+ filePath: string;
22
+ /** Max entries to retain before evicting the oldest (default 10_000). */
23
+ maxEntries?: number;
24
+ /** Max age (ms) before an entry is evicted (default 30 days). */
25
+ maxAgeMs?: number;
26
+ /** Injected for testability. Default: Date.now. */
27
+ now?: () => number;
28
+ }
29
+ export declare class ProcessedIdStore {
30
+ private readonly filePath;
31
+ private readonly maxEntries;
32
+ private readonly maxAgeMs;
33
+ private readonly now;
34
+ private entries;
35
+ constructor(opts: ProcessedIdStoreOptions);
36
+ /** True if the id has been marked + has not been evicted. */
37
+ hasProcessed(id: string): boolean;
38
+ /** Mark this id processed at now() and persist. Idempotent (re-marking is a no-op
39
+ * beyond refreshing the persisted snapshot). */
40
+ markProcessed(id: string): void;
41
+ /** Test helper: current entry count. */
42
+ size(): number;
43
+ private load;
44
+ private persist;
45
+ private evictExpired;
46
+ private evictOverflow;
47
+ }
48
+ //# sourceMappingURL=ProcessedIdStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProcessedIdStore.d.ts","sourceRoot":"","sources":["../../src/messaging/ProcessedIdStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,MAAM,WAAW,uBAAuB;IACtC,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAgBD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,OAAO,CAA+B;gBAElC,IAAI,EAAE,uBAAuB;IASzC,6DAA6D;IAC7D,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAKjC;qDACiD;IACjD,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAS/B,wCAAwC;IACxC,IAAI,IAAI,MAAM;IAKd,OAAO,CAAC,IAAI;IAmBZ,OAAO,CAAC,OAAO;IAef,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,aAAa;CAStB"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * ProcessedIdStore — small persistent set of recently-processed a2a marker `id`s, so
3
+ * Telegram retries / adapter restarts can't double-inject the same prompt (spec
4
+ * MENTOR-LIVE-READINESS §Fix 2a "Processed-id ledger" + Codey's round-2 design point).
5
+ *
6
+ * Bounded by `maxEntries` (default 10_000) AND `maxAgeMs` (default 30 days) — whichever
7
+ * evicts first. **Bounded eviction is the same surface adversarial F2 worried about**:
8
+ * an `id` that ages out is no longer "duplicate" and could be re-injected by a replayer
9
+ * who captured it. The marker's `ts` + skew-window (24h default) is the defense at the
10
+ * primitive layer; this store's eviction policy is set generously so legitimate retries
11
+ * within hours/days are still de-duped.
12
+ *
13
+ * Convention note: instar prefers SQLite for durable dedup ledgers (MessageProcessingLedger,
14
+ * CommitmentTracker). This store uses a small JSON file because the working set is small
15
+ * (per-mentee processed-ids over 30d), the file is rewritten atomically on every mark, and
16
+ * there's no concurrent-writer story to need WAL. A future bump to SQLite is a drop-in
17
+ * replacement at the class boundary.
18
+ */
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { SafeFsExecutor } from '../core/SafeFsExecutor.js';
22
+ const DEFAULT_MAX_ENTRIES = 10_000;
23
+ const DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
24
+ export class ProcessedIdStore {
25
+ filePath;
26
+ maxEntries;
27
+ maxAgeMs;
28
+ now;
29
+ entries;
30
+ constructor(opts) {
31
+ this.filePath = opts.filePath;
32
+ this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
33
+ this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
34
+ this.now = opts.now ?? Date.now;
35
+ this.entries = new Map();
36
+ this.load();
37
+ }
38
+ /** True if the id has been marked + has not been evicted. */
39
+ hasProcessed(id) {
40
+ this.evictExpired();
41
+ return this.entries.has(id);
42
+ }
43
+ /** Mark this id processed at now() and persist. Idempotent (re-marking is a no-op
44
+ * beyond refreshing the persisted snapshot). */
45
+ markProcessed(id) {
46
+ if (!this.entries.has(id)) {
47
+ this.entries.set(id, { ts: this.now() });
48
+ }
49
+ this.evictExpired();
50
+ this.evictOverflow();
51
+ this.persist();
52
+ }
53
+ /** Test helper: current entry count. */
54
+ size() {
55
+ this.evictExpired();
56
+ return this.entries.size;
57
+ }
58
+ load() {
59
+ if (!fs.existsSync(this.filePath))
60
+ return;
61
+ try {
62
+ const raw = fs.readFileSync(this.filePath, 'utf-8');
63
+ const parsed = JSON.parse(raw);
64
+ if (parsed && parsed.v === 1 && parsed.entries && typeof parsed.entries === 'object') {
65
+ for (const [id, ts] of Object.entries(parsed.entries)) {
66
+ if (typeof ts === 'number' && Number.isFinite(ts)) {
67
+ this.entries.set(id, { ts });
68
+ }
69
+ }
70
+ }
71
+ }
72
+ catch {
73
+ // Corrupted store → start fresh (full re-allow). Better than crashing the recipient
74
+ // — duplicate prompts are recoverable; a dead recipient isn't.
75
+ this.entries = new Map();
76
+ }
77
+ }
78
+ persist() {
79
+ const file = {
80
+ v: 1,
81
+ entries: Object.fromEntries([...this.entries].map(([id, r]) => [id, r.ts])),
82
+ };
83
+ try {
84
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
85
+ SafeFsExecutor.atomicWriteJsonSync(this.filePath, file, { operation: 'ProcessedIdStore.persist' });
86
+ }
87
+ catch (err) {
88
+ // Best-effort persistence; on the next mark we retry.
89
+ // eslint-disable-next-line no-console
90
+ console.warn(`[a2a-processed-id] persist failed (non-fatal) at ${this.filePath}:`, err instanceof Error ? err.message : String(err));
91
+ }
92
+ }
93
+ evictExpired() {
94
+ const cutoff = this.now() - this.maxAgeMs;
95
+ for (const [id, r] of this.entries) {
96
+ if (r.ts < cutoff)
97
+ this.entries.delete(id);
98
+ }
99
+ }
100
+ evictOverflow() {
101
+ if (this.entries.size <= this.maxEntries)
102
+ return;
103
+ // Drop oldest first.
104
+ const sorted = [...this.entries].sort((a, b) => a[1].ts - b[1].ts);
105
+ const dropCount = this.entries.size - this.maxEntries;
106
+ for (let i = 0; i < dropCount; i++) {
107
+ this.entries.delete(sorted[i][0]);
108
+ }
109
+ }
110
+ }
111
+ //# sourceMappingURL=ProcessedIdStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProcessedIdStore.js","sourceRoot":"","sources":["../../src/messaging/ProcessedIdStore.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.36",
3
+ "version": "1.3.37",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-05-27T23:50:57.603Z",
5
- "instarVersion": "1.3.36",
4
+ "generatedAt": "2026-05-27T23:52:19.576Z",
5
+ "instarVersion": "1.3.37",
6
6
  "entryCount": 192,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -15,11 +15,27 @@ The **Release-Readiness Visibility** spec (`docs/specs/RELEASE-READINESS-VISIBIL
15
15
 
16
16
  The fix path for an existing silent stall: the watchdog opens an Attention item; auto-draft fills NEXT.md but BOTH publish gates refuse it until a human reviews each section (replacing the marker with a hash-locked receipt); rollback flips a state-file kill-switch loudly.
17
17
 
18
+ ---
19
+
20
+ **Agent-to-agent Telegram comms primitive — receiver-side storage primitives.** Two new
21
+ internal modules the next update will use to wire the recipient side of the a2a primitive,
22
+ both **dark** (no caller yet):
23
+
24
+ - `AgentTelegramLedger` — append-only JSONL audit trail of every a2a send and every a2a
25
+ receive decision (routed or dropped, with the drop reason). Best-effort + non-throwing
26
+ (an audit-write failure must not crash a tick).
27
+ - `ProcessedIdStore` — bounded persistent set of recently-processed marker `id`s, so
28
+ Telegram retries or adapter restarts can't double-inject the same prompt. Bounded by
29
+ `maxEntries` (10k default) AND `maxAgeMs` (30d default). Backed by an atomic-write JSON
30
+ file via SafeFsExecutor; corrupt-file recovery (starts fresh rather than crash the
31
+ recipient).
32
+
18
33
  ## What to Tell Your User
19
34
 
20
35
  - **I'll notice if my own ship is stuck**: when changes pile up and a release stops going out, I now raise a single, calm flag on my attention list — and the longer it sits, the louder that flag gets. You don't have to remember to check whether I'm actually shipping; I'll tell you when I'm not.
21
36
  - **The release notes draft themselves**: instead of waiting for someone to write release notes from scratch, the toolchain now sketches a starter version from the change list. A human still has to read and sign off on each section before it can publish — the safety net stays in place; it just isn't a blank page anymore.
22
37
  - **A merged feature can no longer be invisible to my own systems**: my feature board used to read whatever was on my laptop, which sometimes meant brand-new merged features didn't show up. It can now read the real shared copy directly, so the newest work stops being the most-hidden work.
38
+ - The agent-to-agent Telegram comms infrastructure continues to land in pieces; still plumbing, nothing user-visible changes today.
23
39
 
24
40
  This change is off by default everywhere. I'll dogfood it on myself first.
25
41
 
@@ -31,9 +47,13 @@ This change is off by default everywhere. I'll dogfood it on myself first.
31
47
  | Auto-draft an upgrade guide | `node scripts/analyze-release.js --draft-guide` (writes `upgrades/NEXT.md` with unreviewed markers) |
32
48
  | Canonical-ref spec scan | Set `featureRollout.canonicalRefScan: true` and `featureRollout.canonicalRemote` in `.instar/config.json` |
33
49
  | Enable the watchdog | Set `monitoring.releaseReadiness.enabled: true` in `.instar/config.json`; the `release-readiness-check` job runs every 6h (still ships disabled — flip it on per agent) |
50
+ | `AgentTelegramLedger` | Internal — `new AgentTelegramLedger(defaultLedgerPaths(stateDir))`; `appendSent(row)` + `appendReceived(row)`; JSONL, best-effort non-throwing |
51
+ | `ProcessedIdStore` | Internal — `new ProcessedIdStore({filePath, maxEntries?, maxAgeMs?})`; `hasProcessed(id)` + `markProcessed(id)`; bounded + persistent |
34
52
 
35
53
  ## Evidence
36
54
 
37
55
  The Layer B real-I/O E2E (`tests/e2e/release-readiness-live.test.ts`) reproduces the original silent-stall shape against a fixture instar repo (real git fetch + real `analyze-release.js` subprocess + real `merge-base`) and asserts that exactly one Attention item is raised for an aged, blocked backlog. The Layer A unit tests (`tests/unit/analyze-release-draft-guide.test.ts` and `tests/unit/upgrade-guide-autodraft-review.test.ts`) reproduce the placeholder-text bypass adversarial review caught (the analyzer's `'Review the commit for specifics'` fallback that previously could pass the section-presence gate) and confirm the new validator blocks it. The Layer C unit test (`tests/unit/featureRolloutScan-canonical.test.ts`) reproduces the exact bug — a spec deleted from the local working tree but committed to main — and confirms the canonical scan still detects it as merged. ~64 tests added across three layered tiers, all green.
38
56
 
39
57
  Additionally, this very NEXT.md authoring revealed the meta-failure: the two PRs above merged but their publishes silently skipped because **NEXT.md didn't exist** — exactly the silent-skip mechanism Layer A + Layer B were built to surface. This guide is the unblock; once it publishes, the watchdog this guide describes goes live and can catch the same shape next time.
58
+
59
+ Plus, the a2a comms storage primitives shipping here have 8 unit tests, all green: ledger writes to distinct files (append-only JSONL, one row per event); ledger captures the drop reason on routing-matrix drops; ledger never throws when the target is unwritable (best-effort guard tested by pointing at an unwritable path); store marks + recalls + persists across re-open; store evicts by `maxAgeMs` and by `maxEntries` (oldest first); store survives a corrupted file (starts fresh, doesn't crash); re-marking the same id is idempotent (first-seen-ts preserved).
@@ -0,0 +1,68 @@
1
+ # Side-Effects Review — a2a audit ledger + processed-id store (PR 3a)
2
+
3
+ **Spec:** `docs/specs/MENTOR-LIVE-READINESS-SPEC.md` §Fix 2a "Round-trip audit ledger" +
4
+ "Processed-id ledger" + Codey's round-2 idempotency-on-receive design point. PR 3 of the
5
+ staged build, part a.
6
+ **Change:** Two new storage primitive modules the receiver wiring (PR 3b) will consume:
7
+ `AgentTelegramLedger` (append-only JSONL of sent + received audit rows) and
8
+ `ProcessedIdStore` (bounded persistent set of recently-processed marker `id`s for Telegram
9
+ retry / restart idempotency). Both **dark** — no caller yet; PR 3b wires the recipient
10
+ handler at `server.ts` and uses these.
11
+ **Files:** `src/messaging/AgentTelegramLedger.ts` (new), `src/messaging/ProcessedIdStore.ts`
12
+ (new), `tests/unit/messaging/AgentTelegramLedger.test.ts` (new — covers both).
13
+
14
+ ## The seven questions
15
+
16
+ 1. **Over-block.** N/A — no gate. The ledger always writes; the store only blocks on a
17
+ recorded `id` (the idempotency guarantee).
18
+ 2. **Under-block.** Ledger writes are **best-effort + non-throwing** — an audit-write
19
+ failure must not crash a tick (spec note: "silent drops make Stage-B forensics painful,
20
+ but a crashed tick is worse"). Tested: pointing the ledger at an unwritable path → no
21
+ throw. The store's persistence is also best-effort (warn + continue on persist failure,
22
+ retry on next mark).
23
+ 3. **Level-of-abstraction fit.** Each module owns ONE thing — ledger appends rows; store
24
+ answers "have I seen this id." Both injectable for PR 3b's wiring + tests. JSONL append
25
+ is intentional (atomic at line granularity on POSIX for `<PIPE_BUF`); SafeFsExecutor's
26
+ atomic-write-replace is wrong for append (documented in module header so a future
27
+ reviewer doesn't "fix" it incorrectly).
28
+ 4. **Signal vs authority.** N/A.
29
+ 5. **Interactions — both modules are dark.** No caller, no runtime side-effect. PR 3b
30
+ integrates them into the recipient handler. The ReceiveAuditRow shape is the wire
31
+ contract for Stage-B forensics + future debugging.
32
+ 6. **External surfaces.** None new. Default paths are `{stateDir}/a2a-sent.jsonl` +
33
+ `{stateDir}/a2a-received.jsonl` + (PR 3b chooses) `{stateDir}/a2a-processed-ids.json`.
34
+ Caller may override.
35
+ 7. **Rollback cost.** Trivial — revert removes two unused modules. No data, no migration.
36
+
37
+ ## Convention notes
38
+
39
+ - **JSONL ≠ SQLite for these two cases.** The MessageProcessingLedger header explicitly
40
+ prefers SQLite for dedup ledgers ("NOT a new ad-hoc JSON file") — and we follow that
41
+ spirit by isolating the data behind a class boundary (a SQLite swap-in is one constructor
42
+ away). The current JSON-file backing is justified for `ProcessedIdStore` because the
43
+ working set is small (bounded 10k entries / 30d), atomic-write-rewrite is cheap at that
44
+ size, and there's no concurrent-writer story to need WAL. For `AgentTelegramLedger`,
45
+ JSONL append IS the right shape (forensic trail, append-only, line-atomic).
46
+ - **Eviction policy + the round-2 adversarial F2 surface.** The store evicts by
47
+ `maxEntries` (10k) OR `maxAgeMs` (30d) — whichever first. A replayer who captured an `id`
48
+ outside that window could re-inject it; the **defense at that layer is the marker's
49
+ `ts` + `skewWindowMs` (24h)** added in PR 1 (already shipped). The store's job is
50
+ "de-dup within the legitimate-retry window," not "absolute replay protection." Documented.
51
+
52
+ ## Testing
53
+
54
+ 8 unit tests, all green:
55
+ - **Ledger**: appendSent + appendReceived to distinct files (+ JSONL line-per-row); drop
56
+ row with `dropReason` captured (the routing-matrix audit trail); **never throws** when
57
+ the target is unwritable (best-effort guard tested).
58
+ - **Store**: mark + recall + persist-across-reopen; eviction by `maxAgeMs`; eviction by
59
+ `maxEntries` (oldest first); corrupt-file recovery (start fresh, don't crash); idempotent
60
+ re-mark (first-seen-ts preserved).
61
+ - `tsc --noEmit` clean. Both modules dark (no caller).
62
+
63
+ ## Migration parity
64
+
65
+ None — two new unwired modules. No agent-installed file changes, no config consumed yet.
66
+ PR 3b adds the recipient handler wiring + `mentor.processedIdStorePath` (or similar) to
67
+ config defaults; PR 3c (mentor consumer) adds the file-outbox retirement + dead-config
68
+ removal migrations per the spec's §Migration parity.