instar 1.3.550 → 1.3.552

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.
Files changed (35) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +25 -0
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +24 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  8. package/dist/core/PostUpdateMigrator.js +32 -0
  9. package/dist/core/PostUpdateMigrator.js.map +1 -1
  10. package/dist/core/RemoteAckStore.d.ts +71 -0
  11. package/dist/core/RemoteAckStore.d.ts.map +1 -0
  12. package/dist/core/RemoteAckStore.js +144 -0
  13. package/dist/core/RemoteAckStore.js.map +1 -0
  14. package/dist/core/types.d.ts +24 -1
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/core/types.js.map +1 -1
  17. package/dist/scaffold/templates.d.ts.map +1 -1
  18. package/dist/scaffold/templates.js +2 -0
  19. package/dist/scaffold/templates.js.map +1 -1
  20. package/dist/scheduler/JobScheduler.d.ts +33 -0
  21. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  22. package/dist/scheduler/JobScheduler.js +75 -0
  23. package/dist/scheduler/JobScheduler.js.map +1 -1
  24. package/dist/server/routes.d.ts.map +1 -1
  25. package/dist/server/routes.js +186 -0
  26. package/dist/server/routes.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/data/builtin-manifest.json +64 -64
  29. package/src/data/state-coherence-registry.json +12 -0
  30. package/src/scaffold/templates.ts +2 -0
  31. package/upgrades/1.3.551.md +23 -0
  32. package/upgrades/1.3.552.md +23 -0
  33. package/upgrades/side-effects/ws41-durable-ack.md +37 -0
  34. package/upgrades/side-effects/ws43-role-guard.md +40 -0
  35. package/upgrades/ws43-role-guard.eli16.md +47 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * RemoteAckStore — durable queue for operator-bound attention acks that target
3
+ * an attention item OWNED BY A DIFFERENT MACHINE (WS4.1 follow-up, CMT-1416).
4
+ *
5
+ * The problem this closes: when an operator acknowledges/resolves a pooled
6
+ * attention item whose owner is briefly offline, the ack PATCH is lost — the
7
+ * operator's intent evaporates and the item reappears OPEN on the owner's
8
+ * return. This store persists the ack INTENT (with the authenticated operator
9
+ * principal that performed it) so it survives the owner being dark; a drain
10
+ * tick + a boot sweep re-deliver it when the owner comes back.
11
+ *
12
+ * Design constraints (mirrors RemoteCloseAudit / PendingInboundStore):
13
+ * - Append-then-compact JSONL under logs/ — no DB dependency.
14
+ * - Each pending ack is idempotent on (itemId, targetMachineId): a re-ack of
15
+ * the same item just refreshes the intent, never stacks duplicates.
16
+ * - The operator principal is carried as data the OWNER revalidates at apply
17
+ * time — this store never authorizes anything, it only remembers intent.
18
+ * - Best-effort writes; a failed append/compact logs loudly (an unrecorded
19
+ * ack-intent deserves a trace) but never throws into the ack path.
20
+ *
21
+ * This file ships behind the dark gate multiMachine.seamlessness.ws41DurableAck:
22
+ * when off, the route that feeds it 503s and the store is never constructed, so
23
+ * a single-machine or flag-off agent is a strict no-op.
24
+ */
25
+ export interface PendingRemoteAck {
26
+ /** Attention item id on the OWNING machine. */
27
+ itemId: string;
28
+ /** Registry lookup key for the owning machine (never a URL). */
29
+ targetMachineId: string;
30
+ /** Normalized attention status the operator chose (e.g. DONE, ACKNOWLEDGED). */
31
+ status: string;
32
+ /** Authenticated operator principal that performed the ack (data the owner
33
+ * revalidates — never trusted as authorization by this store). */
34
+ operatorUid: string;
35
+ /** Operator display name (for the owner's audit; not load-bearing). */
36
+ operatorDisplayName?: string;
37
+ /** When the operator's intent was first captured (ISO). */
38
+ enqueuedAt: string;
39
+ /** Delivery attempts so far (drain backoff / give-up bound). */
40
+ attempts: number;
41
+ /** Last delivery attempt outcome class, for observability. */
42
+ lastOutcome?: 'pending' | 'unreachable' | 'rejected' | 'stale-superseded';
43
+ }
44
+ export declare class RemoteAckStore {
45
+ private readonly log;
46
+ private readonly filePath;
47
+ /** Keyed on `${itemId}::${targetMachineId}` — idempotent intent. */
48
+ private readonly pending;
49
+ private loaded;
50
+ constructor(stateDir: string, log?: (msg: string) => void);
51
+ private key;
52
+ private ensureLoaded;
53
+ private append;
54
+ /**
55
+ * Record (or refresh) the operator's ack intent for an item owned elsewhere.
56
+ * Idempotent on (itemId, targetMachineId): a repeat ack updates status/principal
57
+ * and resets attempts rather than stacking a second pending row.
58
+ */
59
+ enqueue(intent: Omit<PendingRemoteAck, 'enqueuedAt' | 'attempts' | 'lastOutcome'>): PendingRemoteAck;
60
+ /** Mark a delivery attempt's outcome (drain observability + backoff input). */
61
+ recordAttempt(itemId: string, targetMachineId: string, outcome: PendingRemoteAck['lastOutcome']): void;
62
+ /** Remove a delivered (or owner-rejected-as-stale) intent — terminal. */
63
+ resolve(itemId: string, targetMachineId: string): void;
64
+ /** All still-pending intents (for the drain tick / boot sweep / observability). */
65
+ list(): PendingRemoteAck[];
66
+ /** Pending intents targeting one machine (drain when that peer comes online). */
67
+ listForMachine(targetMachineId: string): PendingRemoteAck[];
68
+ /** Count of still-pending intents (cheap status read). */
69
+ get size(): number;
70
+ }
71
+ //# sourceMappingURL=RemoteAckStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RemoteAckStore.d.ts","sourceRoot":"","sources":["../../src/core/RemoteAckStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,eAAe,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;IACf;uEACmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,SAAS,GAAG,aAAa,GAAG,UAAU,GAAG,kBAAkB,CAAC;CAC3E;AAED,qBAAa,cAAc;IAQvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,oEAAoE;IACpE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuC;IAC/D,OAAO,CAAC,MAAM,CAAS;gBAGrB,QAAQ,EAAE,MAAM,EACC,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAkB;IAK3D,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,YAAY;IA0BpB,OAAO,CAAC,MAAM;IAgBd;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,gBAAgB,EAAE,YAAY,GAAG,UAAU,GAAG,aAAa,CAAC,GAAG,gBAAgB;IAepG,+EAA+E;IAC/E,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,CAAC,aAAa,CAAC,GAAG,IAAI;IAUtG,yEAAyE;IACzE,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI;IAgBtD,mFAAmF;IACnF,IAAI,IAAI,gBAAgB,EAAE;IAK1B,iFAAiF;IACjF,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,gBAAgB,EAAE;IAI3D,0DAA0D;IAC1D,IAAI,IAAI,IAAI,MAAM,CAGjB;CACF"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * RemoteAckStore — durable queue for operator-bound attention acks that target
3
+ * an attention item OWNED BY A DIFFERENT MACHINE (WS4.1 follow-up, CMT-1416).
4
+ *
5
+ * The problem this closes: when an operator acknowledges/resolves a pooled
6
+ * attention item whose owner is briefly offline, the ack PATCH is lost — the
7
+ * operator's intent evaporates and the item reappears OPEN on the owner's
8
+ * return. This store persists the ack INTENT (with the authenticated operator
9
+ * principal that performed it) so it survives the owner being dark; a drain
10
+ * tick + a boot sweep re-deliver it when the owner comes back.
11
+ *
12
+ * Design constraints (mirrors RemoteCloseAudit / PendingInboundStore):
13
+ * - Append-then-compact JSONL under logs/ — no DB dependency.
14
+ * - Each pending ack is idempotent on (itemId, targetMachineId): a re-ack of
15
+ * the same item just refreshes the intent, never stacks duplicates.
16
+ * - The operator principal is carried as data the OWNER revalidates at apply
17
+ * time — this store never authorizes anything, it only remembers intent.
18
+ * - Best-effort writes; a failed append/compact logs loudly (an unrecorded
19
+ * ack-intent deserves a trace) but never throws into the ack path.
20
+ *
21
+ * This file ships behind the dark gate multiMachine.seamlessness.ws41DurableAck:
22
+ * when off, the route that feeds it 503s and the store is never constructed, so
23
+ * a single-machine or flag-off agent is a strict no-op.
24
+ */
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ export class RemoteAckStore {
28
+ log;
29
+ filePath;
30
+ /** Keyed on `${itemId}::${targetMachineId}` — idempotent intent. */
31
+ pending = new Map();
32
+ loaded = false;
33
+ constructor(stateDir, log = console.log) {
34
+ this.log = log;
35
+ this.filePath = path.join(stateDir, '..', 'logs', 'remote-ack-queue.jsonl');
36
+ }
37
+ key(itemId, targetMachineId) {
38
+ return `${itemId}::${targetMachineId}`;
39
+ }
40
+ ensureLoaded() {
41
+ if (this.loaded)
42
+ return;
43
+ this.loaded = true;
44
+ try {
45
+ const raw = fs.readFileSync(this.filePath, 'utf-8');
46
+ for (const line of raw.split('\n')) {
47
+ const t = line.trim();
48
+ if (!t)
49
+ continue;
50
+ try {
51
+ const entry = JSON.parse(t);
52
+ const k = this.key(entry.itemId, entry.targetMachineId);
53
+ if (entry._deleted) {
54
+ this.pending.delete(k);
55
+ }
56
+ else {
57
+ this.pending.set(k, entry);
58
+ }
59
+ }
60
+ catch {
61
+ // @silent-fallback-ok — a torn last line (crash mid-append) is skipped;
62
+ // the rest of the durable log still loads.
63
+ }
64
+ }
65
+ }
66
+ catch {
67
+ // @silent-fallback-ok — absent file reads as empty queue (first run).
68
+ }
69
+ }
70
+ append(entry) {
71
+ try {
72
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
73
+ /* state-registry: remote-ack-queue */
74
+ fs.appendFileSync(this.filePath, JSON.stringify(entry) + '\n');
75
+ }
76
+ catch (err) {
77
+ // Loud, not silent: a lost ack-intent reappears as a re-OPENed item to the
78
+ // operator later — worth a trace even when the log file is the failure.
79
+ this.log(`[remote-ack] QUEUE APPEND FAILED (intent held in memory only): ${err instanceof Error ? err.message : String(err)}`);
80
+ }
81
+ }
82
+ /**
83
+ * Record (or refresh) the operator's ack intent for an item owned elsewhere.
84
+ * Idempotent on (itemId, targetMachineId): a repeat ack updates status/principal
85
+ * and resets attempts rather than stacking a second pending row.
86
+ */
87
+ enqueue(intent) {
88
+ this.ensureLoaded();
89
+ const k = this.key(intent.itemId, intent.targetMachineId);
90
+ const existing = this.pending.get(k);
91
+ const entry = {
92
+ ...intent,
93
+ enqueuedAt: existing?.enqueuedAt ?? new Date().toISOString(),
94
+ attempts: 0,
95
+ lastOutcome: 'pending',
96
+ };
97
+ this.pending.set(k, entry);
98
+ this.append(entry);
99
+ return entry;
100
+ }
101
+ /** Mark a delivery attempt's outcome (drain observability + backoff input). */
102
+ recordAttempt(itemId, targetMachineId, outcome) {
103
+ this.ensureLoaded();
104
+ const k = this.key(itemId, targetMachineId);
105
+ const entry = this.pending.get(k);
106
+ if (!entry)
107
+ return;
108
+ entry.attempts += 1;
109
+ entry.lastOutcome = outcome;
110
+ this.append(entry);
111
+ }
112
+ /** Remove a delivered (or owner-rejected-as-stale) intent — terminal. */
113
+ resolve(itemId, targetMachineId) {
114
+ this.ensureLoaded();
115
+ const k = this.key(itemId, targetMachineId);
116
+ if (!this.pending.has(k))
117
+ return;
118
+ this.pending.delete(k);
119
+ this.append({
120
+ itemId,
121
+ targetMachineId,
122
+ status: '',
123
+ operatorUid: '',
124
+ enqueuedAt: new Date().toISOString(),
125
+ attempts: 0,
126
+ _deleted: true,
127
+ });
128
+ }
129
+ /** All still-pending intents (for the drain tick / boot sweep / observability). */
130
+ list() {
131
+ this.ensureLoaded();
132
+ return Array.from(this.pending.values());
133
+ }
134
+ /** Pending intents targeting one machine (drain when that peer comes online). */
135
+ listForMachine(targetMachineId) {
136
+ return this.list().filter((e) => e.targetMachineId === targetMachineId);
137
+ }
138
+ /** Count of still-pending intents (cheap status read). */
139
+ get size() {
140
+ this.ensureLoaded();
141
+ return this.pending.size;
142
+ }
143
+ }
144
+ //# sourceMappingURL=RemoteAckStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RemoteAckStore.js","sourceRoot":"","sources":["../../src/core/RemoteAckStore.ts"],"names":[],"mappings":"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"}
@@ -357,6 +357,18 @@ export interface JobDefinition {
357
357
  * re-run must be a deliberate per-job choice. Older agents' job parsers
358
358
  * ignore unknown fields (additive-safe). */
359
359
  resumeOnReap?: boolean;
360
+ /** MULTI-MACHINE-SEAMLESSNESS-SPEC §WS4.3 role-guard-at-spawn — opt this job
361
+ * IN as a STATE-WRITING job. A state-writing job mutates shared/replicated
362
+ * state that only the writable owner (the lease-holder) may touch; it must
363
+ * NOT spawn on a read-only standby. When `multiMachine.seamlessness.ws43RoleGuard`
364
+ * is on and this machine does not hold the lease, the scheduler refuses the
365
+ * spawn at the spawn boundary (the TOCTOU re-check — a machine awake at boot
366
+ * can demote mid-run while its cron tasks keep firing). The writable owner's
367
+ * own scheduler runs the job (the cron fires on every machine; only the
368
+ * owner's pass clears the guard), so the refusal re-routes by construction.
369
+ * Default-absent = NOT state-writing → never guarded (additive-safe; older
370
+ * agents' parsers ignore the unknown field). */
371
+ writesState?: boolean;
360
372
  /** Tags for filtering/grouping */
361
373
  tags?: string[];
362
374
  /** Telegram topic ID this job reports to (auto-created if not set) */
@@ -985,7 +997,7 @@ export interface RelationshipManagerConfig {
985
997
  */
986
998
  intelligence?: IntelligenceProvider;
987
999
  }
988
- export type SkipReason = 'disabled' | 'paused' | 'quota' | 'memory-pressure' | 'capacity' | 'claimed' | 'machine-scope' | 'already-running' | 'gate';
1000
+ export type SkipReason = 'disabled' | 'paused' | 'quota' | 'memory-pressure' | 'capacity' | 'claimed' | 'machine-scope' | 'already-running' | 'role-guard' | 'gate';
989
1001
  /**
990
1002
  * Result of a canRunJob gate check. The callback may return a plain boolean
991
1003
  * (legacy) or this richer form so the scheduler can log/track the actual
@@ -2010,6 +2022,17 @@ export interface MultiMachineConfig {
2010
2022
  * config defaults (the gate decides) — a literal `false` would force-dark dev.
2011
2023
  */
2012
2024
  ws44PoolLinks?: boolean;
2025
+ /**
2026
+ * WS4.3 role-guard-at-spawn (§WS4.3 / F8, F21; CMT-1416). When on, the
2027
+ * scheduler refuses to spawn a STATE-WRITING job (JobDefinition.writesState)
2028
+ * on a machine that does NOT hold the lease — the spawn-boundary TOCTOU
2029
+ * re-check that closes the window where a machine awake at boot demotes to a
2030
+ * read-only standby mid-run while its cron tasks keep firing. DEFAULT
2031
+ * (undefined/false) = strict no-op (byte-for-byte today's behavior). The
2032
+ * writable owner's own scheduler runs the job; a refusal raises ONE deduped
2033
+ * attention item. Single-machine agents always hold the lease → never fires.
2034
+ */
2035
+ ws43RoleGuard?: boolean;
2013
2036
  /**
2014
2037
  * WS4.4 (f) load-shed: when the fronting machine is over this 1-min
2015
2038
  * load-per-core threshold, holder-resolution serves the last-cached