instar 1.3.588 → 1.3.589
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +93 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/LiveTestArtifactStore.d.ts +129 -0
- package/dist/core/LiveTestArtifactStore.d.ts.map +1 -0
- package/dist/core/LiveTestArtifactStore.js +184 -0
- package/dist/core/LiveTestArtifactStore.js.map +1 -0
- package/dist/core/LiveTestGate.d.ts +54 -0
- package/dist/core/LiveTestGate.d.ts.map +1 -0
- package/dist/core/LiveTestGate.js +124 -0
- package/dist/core/LiveTestGate.js.map +1 -0
- package/dist/core/LiveTestHarness.d.ts +99 -0
- package/dist/core/LiveTestHarness.d.ts.map +1 -0
- package/dist/core/LiveTestHarness.js +102 -0
- package/dist/core/LiveTestHarness.js.map +1 -0
- package/dist/core/LocalSessionOwnershipStore.d.ts +56 -0
- package/dist/core/LocalSessionOwnershipStore.d.ts.map +1 -0
- package/dist/core/LocalSessionOwnershipStore.js +130 -0
- package/dist/core/LocalSessionOwnershipStore.js.map +1 -0
- package/dist/core/OwnershipApplier.d.ts +78 -0
- package/dist/core/OwnershipApplier.d.ts.map +1 -0
- package/dist/core/OwnershipApplier.js +101 -0
- package/dist/core/OwnershipApplier.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +9 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +12 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +3 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +2 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +4 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +58 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +2 -0
- package/upgrades/1.3.589.md +68 -0
- package/upgrades/side-effects/live-user-channel-proof-standard.md +54 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiveTestHarness — the user-role live-test runner (spec
|
|
3
|
+
* docs/specs/live-user-channel-proof-standard.md §5). It drives a feature
|
|
4
|
+
* end-to-end AS THE USER through the real surface (Telegram AND Slack for a channel
|
|
5
|
+
* feature), recording a signed PASS/FAIL scenario matrix as the artifact the
|
|
6
|
+
* completion gate reads.
|
|
7
|
+
*
|
|
8
|
+
* This module is the harness CORE: it runs a scenario matrix over an INJECTED
|
|
9
|
+
* `ChannelDriver` (send / awaitReply / isDemoChannel), so it is unit-testable with a
|
|
10
|
+
* fake driver. The REAL Telegram + Slack drivers (real-account drive, §5.4, demo
|
|
11
|
+
* channels) and the Playwright dashboard driver wire into this same interface.
|
|
12
|
+
*
|
|
13
|
+
* Structural safety (§5.3): a volatile/permission scenario is REFUSED before any
|
|
14
|
+
* send unless its target channel is a demo channel — a structural throw, not a
|
|
15
|
+
* convention, so a destructive scenario can never touch the live operator channel.
|
|
16
|
+
*
|
|
17
|
+
* Determinism (§5.6/§4.4): the PASS/FAIL verdict rests on DETERMINISTIC protocol
|
|
18
|
+
* evidence (the reply text + the responder machine id captured from the real
|
|
19
|
+
* platform), not a fuzzy judgment. (An optional Tier-1 semantic supervisor for
|
|
20
|
+
* natural-language expectations is a follow-on; the core encodes exact checks.)
|
|
21
|
+
*/
|
|
22
|
+
export class HarnessVolatileChannelError extends Error {
|
|
23
|
+
constructor(scenarioId, surface, channelId) {
|
|
24
|
+
super(`refusing volatile/permission scenario "${scenarioId}" on non-demo channel ${surface}:${channelId} (§5.3 — volatile scenarios run only on demo channels)`);
|
|
25
|
+
this.name = 'HarnessVolatileChannelError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class LiveTestHarness {
|
|
29
|
+
d;
|
|
30
|
+
constructor(deps) {
|
|
31
|
+
this.d = deps;
|
|
32
|
+
}
|
|
33
|
+
now() { return (this.d.now ?? Date.now)(); }
|
|
34
|
+
log(m) { this.d.logger?.(`[live-test-harness] ${m}`); }
|
|
35
|
+
/**
|
|
36
|
+
* Run the scenario matrix and write the signed artifact. `runId` lets the caller
|
|
37
|
+
* pin a stable id (defaults to a now-stamped id). THROWS HarnessVolatileChannelError
|
|
38
|
+
* if any volatile/permission scenario targets a non-demo channel (fail-fast, before
|
|
39
|
+
* any send) — the §5.3 structural guard.
|
|
40
|
+
*/
|
|
41
|
+
async run(matrix, opts = {}) {
|
|
42
|
+
// §5.3 PRE-FLIGHT: refuse the whole run if a volatile/permission scenario points
|
|
43
|
+
// at a non-demo channel — before a single message is sent.
|
|
44
|
+
for (const s of matrix.scenarios) {
|
|
45
|
+
if (s.volatility !== 'safe' && !this.d.driver.isDemoChannel(s.surface, s.channelId)) {
|
|
46
|
+
throw new HarnessVolatileChannelError(s.id, s.surface, s.channelId);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const rows = [];
|
|
50
|
+
for (const s of matrix.scenarios) {
|
|
51
|
+
rows.push(await this.runScenario(s));
|
|
52
|
+
}
|
|
53
|
+
const runId = opts.runId ?? `run-${this.now()}`;
|
|
54
|
+
const artifact = {
|
|
55
|
+
featureId: matrix.featureId,
|
|
56
|
+
runId,
|
|
57
|
+
surfaces: matrix.surfaces,
|
|
58
|
+
riskCategories: matrix.riskCategories,
|
|
59
|
+
scenarios: rows,
|
|
60
|
+
createdAt: new Date(this.now()).toISOString(),
|
|
61
|
+
runnerFingerprint: this.d.runnerFingerprint,
|
|
62
|
+
};
|
|
63
|
+
const entry = this.d.store.write(artifact);
|
|
64
|
+
return { artifact, entry };
|
|
65
|
+
}
|
|
66
|
+
async runScenario(s) {
|
|
67
|
+
const timeoutMs = s.timeoutMs ?? this.d.defaultTimeoutMs ?? 30_000;
|
|
68
|
+
const maxRetries = this.d.maxReplyRetries ?? 2;
|
|
69
|
+
const base = { id: s.id, description: s.description, surface: s.surface, riskCategory: s.riskCategory, verdict: 'FAIL' };
|
|
70
|
+
try {
|
|
71
|
+
const sent = await this.d.driver.send(s.surface, s.channelId, s.input);
|
|
72
|
+
let reply = null;
|
|
73
|
+
for (let attempt = 0; attempt <= maxRetries && !reply; attempt++) {
|
|
74
|
+
reply = await this.d.driver.awaitReply(s.surface, s.channelId, { timeoutMs, afterMessageId: sent.messageId });
|
|
75
|
+
}
|
|
76
|
+
if (!reply) {
|
|
77
|
+
// §5.5: a generic timeout is FAIL (not auto-BLOCKED — that needs an
|
|
78
|
+
// independently-attributed platform outage).
|
|
79
|
+
return { ...base, verdict: 'FAIL', evidence: { channelId: s.channelId, messageIds: [sent.messageId] }, blockedReason: 'no reply within timeout' };
|
|
80
|
+
}
|
|
81
|
+
const evidence = { channelId: s.channelId, messageIds: [sent.messageId, reply.messageId], responderMachineId: reply.responderMachineId };
|
|
82
|
+
// Deterministic assertions (§4.4 protocol evidence is the verdict, not a guess).
|
|
83
|
+
const failures = [];
|
|
84
|
+
if (s.expect.replyContains && !reply.text.includes(s.expect.replyContains))
|
|
85
|
+
failures.push(`reply missing "${s.expect.replyContains}"`);
|
|
86
|
+
if (s.expect.replyNotEmpty && reply.text.trim() === '')
|
|
87
|
+
failures.push('reply empty');
|
|
88
|
+
if (s.expect.responderMachine && reply.responderMachineId !== s.expect.responderMachine)
|
|
89
|
+
failures.push(`responder ${reply.responderMachineId ?? 'unknown'} ≠ expected ${s.expect.responderMachine}`);
|
|
90
|
+
if (failures.length) {
|
|
91
|
+
this.log(`scenario ${s.id} FAIL: ${failures.join('; ')}`);
|
|
92
|
+
return { ...base, verdict: 'FAIL', evidence, blockedReason: failures.join('; ') };
|
|
93
|
+
}
|
|
94
|
+
return { ...base, verdict: 'PASS', evidence };
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
// A driver error is a FAIL with the reason (never a silent skip).
|
|
98
|
+
return { ...base, verdict: 'FAIL', blockedReason: `driver error: ${err instanceof Error ? err.message : String(err)}` };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=LiveTestHarness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveTestHarness.js","sourceRoot":"","sources":["../../src/core/LiveTestHarness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalSessionOwnershipStore — a DURABLE per-session ownership store that survives
|
|
3
|
+
* restarts, replacing the in-memory-only `InMemorySessionOwnershipStore` for the
|
|
4
|
+
* cross-machine session pool.
|
|
5
|
+
*
|
|
6
|
+
* Spec: docs/specs/live-user-channel-proof-standard.md §7.2 (transfer fix). The
|
|
7
|
+
* registry/FSM/CAS logic in SessionOwnershipRegistry is store-agnostic; this swaps
|
|
8
|
+
* the substrate from a process-lifetime Map to per-session JSON files (atomic
|
|
9
|
+
* tmp+rename, mirroring LocalLeaseStore) PLUS an in-memory cache so the routing hot
|
|
10
|
+
* path stays an in-memory read.
|
|
11
|
+
*
|
|
12
|
+
* Why durable matters (the live bug, 2026-06-15): a transfer wrote the target's
|
|
13
|
+
* ownership record into the SOURCE machine's in-memory Map; the record vanished on
|
|
14
|
+
* restart and never crossed machines. Persisting per-session lets the cross-machine
|
|
15
|
+
* `OwnershipApplier` (which materializes records from the REPLICATED coherence-journal
|
|
16
|
+
* placement entries) land a durable record the target machine reads as owner=self.
|
|
17
|
+
*
|
|
18
|
+
* This store alone is single-machine-durable. Cross-machine replication is carried
|
|
19
|
+
* by the coherence-journal placement stream + OwnershipApplier (off the hot path) —
|
|
20
|
+
* exactly the boundary the spec draws: durable LOCALLY here, REPLICATED there.
|
|
21
|
+
*/
|
|
22
|
+
import type { SessionOwnershipRecord } from './SessionOwnership.js';
|
|
23
|
+
import type { SessionOwnershipStore } from './SessionOwnershipRegistry.js';
|
|
24
|
+
export interface LocalSessionOwnershipStoreDeps {
|
|
25
|
+
/** Absolute directory holding one JSON file per session ownership record. */
|
|
26
|
+
dir: string;
|
|
27
|
+
logger?: (msg: string) => void;
|
|
28
|
+
}
|
|
29
|
+
export declare class LocalSessionOwnershipStore implements SessionOwnershipStore {
|
|
30
|
+
private readonly d;
|
|
31
|
+
/** Hot-path read cache: sessionKey → record. Authoritative-on-disk, cached here. */
|
|
32
|
+
private readonly cache;
|
|
33
|
+
/** Whether the full directory has been scanned into the cache (for all()). */
|
|
34
|
+
private scanned;
|
|
35
|
+
constructor(deps: LocalSessionOwnershipStoreDeps);
|
|
36
|
+
private log;
|
|
37
|
+
private filePathFor;
|
|
38
|
+
/** Load one session's record from disk into the cache (idempotent). */
|
|
39
|
+
private loadOne;
|
|
40
|
+
private persist;
|
|
41
|
+
read(sessionKey: string): SessionOwnershipRecord | null;
|
|
42
|
+
/**
|
|
43
|
+
* Fast-forward CAS: the candidate lands only if it MONOTONICALLY advances the
|
|
44
|
+
* committed epoch (`candidate.ownershipEpoch > current.epoch`) — identical to
|
|
45
|
+
* InMemorySessionOwnershipStore + GitLeaseStore, so SessionOwnershipRegistry's FSM
|
|
46
|
+
* is unchanged regardless of substrate. Synchronous, so within this process a stale
|
|
47
|
+
* competing candidate (computed from the same `current`) still loses.
|
|
48
|
+
*/
|
|
49
|
+
casWrite(candidate: SessionOwnershipRecord): {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
observed: SessionOwnershipRecord | null;
|
|
52
|
+
};
|
|
53
|
+
/** Every known ownership record (cache + a one-time disk scan). */
|
|
54
|
+
all(): SessionOwnershipRecord[];
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=LocalSessionOwnershipStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalSessionOwnershipStore.d.ts","sourceRoot":"","sources":["../../src/core/LocalSessionOwnershipStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAE3E,MAAM,WAAW,8BAA8B;IAC7C,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AASD,qBAAa,0BAA2B,YAAW,qBAAqB;IACtE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAiC;IACnD,oFAAoF;IACpF,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6C;IACnE,8EAA8E;IAC9E,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,8BAA8B;IAIhD,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,WAAW;IAInB,uEAAuE;IACvE,OAAO,CAAC,OAAO;IAoBf,OAAO,CAAC,OAAO;IAgBf,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI;IAIvD;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,sBAAsB,GAAG;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,sBAAsB,GAAG,IAAI,CAAA;KAAE;IAUrG,mEAAmE;IACnE,GAAG,IAAI,sBAAsB,EAAE;CAqBhC"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalSessionOwnershipStore — a DURABLE per-session ownership store that survives
|
|
3
|
+
* restarts, replacing the in-memory-only `InMemorySessionOwnershipStore` for the
|
|
4
|
+
* cross-machine session pool.
|
|
5
|
+
*
|
|
6
|
+
* Spec: docs/specs/live-user-channel-proof-standard.md §7.2 (transfer fix). The
|
|
7
|
+
* registry/FSM/CAS logic in SessionOwnershipRegistry is store-agnostic; this swaps
|
|
8
|
+
* the substrate from a process-lifetime Map to per-session JSON files (atomic
|
|
9
|
+
* tmp+rename, mirroring LocalLeaseStore) PLUS an in-memory cache so the routing hot
|
|
10
|
+
* path stays an in-memory read.
|
|
11
|
+
*
|
|
12
|
+
* Why durable matters (the live bug, 2026-06-15): a transfer wrote the target's
|
|
13
|
+
* ownership record into the SOURCE machine's in-memory Map; the record vanished on
|
|
14
|
+
* restart and never crossed machines. Persisting per-session lets the cross-machine
|
|
15
|
+
* `OwnershipApplier` (which materializes records from the REPLICATED coherence-journal
|
|
16
|
+
* placement entries) land a durable record the target machine reads as owner=self.
|
|
17
|
+
*
|
|
18
|
+
* This store alone is single-machine-durable. Cross-machine replication is carried
|
|
19
|
+
* by the coherence-journal placement stream + OwnershipApplier (off the hot path) —
|
|
20
|
+
* exactly the boundary the spec draws: durable LOCALLY here, REPLICATED there.
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
/** Filesystem-safe filename for a session key (topic ids are numeric, but be safe). */
|
|
25
|
+
function sessionFileName(sessionKey) {
|
|
26
|
+
// Keep it reversible-enough for debugging while jailing path traversal: any
|
|
27
|
+
// char outside [A-Za-z0-9._-] becomes '_'. (A '..' sessionKey can never escape dir.)
|
|
28
|
+
return `${sessionKey.replace(/[^A-Za-z0-9._-]/g, '_')}.json`;
|
|
29
|
+
}
|
|
30
|
+
export class LocalSessionOwnershipStore {
|
|
31
|
+
d;
|
|
32
|
+
/** Hot-path read cache: sessionKey → record. Authoritative-on-disk, cached here. */
|
|
33
|
+
cache = new Map();
|
|
34
|
+
/** Whether the full directory has been scanned into the cache (for all()). */
|
|
35
|
+
scanned = false;
|
|
36
|
+
constructor(deps) {
|
|
37
|
+
this.d = deps;
|
|
38
|
+
}
|
|
39
|
+
log(m) {
|
|
40
|
+
this.d.logger?.(`[local-ownership] ${m}`);
|
|
41
|
+
}
|
|
42
|
+
filePathFor(sessionKey) {
|
|
43
|
+
return path.join(this.d.dir, sessionFileName(sessionKey));
|
|
44
|
+
}
|
|
45
|
+
/** Load one session's record from disk into the cache (idempotent). */
|
|
46
|
+
loadOne(sessionKey) {
|
|
47
|
+
if (this.cache.has(sessionKey))
|
|
48
|
+
return this.cache.get(sessionKey);
|
|
49
|
+
try {
|
|
50
|
+
const fp = this.filePathFor(sessionKey);
|
|
51
|
+
if (fs.existsSync(fp)) {
|
|
52
|
+
const rec = JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|
53
|
+
if (rec && typeof rec.ownershipEpoch === 'number' && typeof rec.ownerMachineId === 'string' && rec.sessionKey === sessionKey) {
|
|
54
|
+
this.cache.set(sessionKey, rec);
|
|
55
|
+
return rec;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// @silent-fallback-ok — a corrupt single-session file reads as "no record"
|
|
61
|
+
// (null), so routing treats the topic as unowned and re-places. Ownership is
|
|
62
|
+
// re-established by the next place/claim or by replication; a lost local copy
|
|
63
|
+
// is self-healing, never authoritative-loss (same posture as LocalLeaseStore).
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
persist(rec) {
|
|
68
|
+
this.cache.set(rec.sessionKey, rec);
|
|
69
|
+
try {
|
|
70
|
+
if (!fs.existsSync(this.d.dir))
|
|
71
|
+
fs.mkdirSync(this.d.dir, { recursive: true });
|
|
72
|
+
const fp = this.filePathFor(rec.sessionKey);
|
|
73
|
+
const tmp = `${fp}.${process.pid}.tmp`;
|
|
74
|
+
fs.writeFileSync(tmp, JSON.stringify(rec, null, 2));
|
|
75
|
+
fs.renameSync(tmp, fp); // atomic swap
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
// NOT silent: a persist failure is logged. The in-memory cache still reflects
|
|
79
|
+
// the write so this process stays correct; durability across restart is what's
|
|
80
|
+
// at risk, and the next successful write (or replication) re-establishes it.
|
|
81
|
+
this.log(`persist failed for ${rec.sessionKey}: ${err instanceof Error ? err.message : String(err)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
read(sessionKey) {
|
|
85
|
+
return this.loadOne(sessionKey);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Fast-forward CAS: the candidate lands only if it MONOTONICALLY advances the
|
|
89
|
+
* committed epoch (`candidate.ownershipEpoch > current.epoch`) — identical to
|
|
90
|
+
* InMemorySessionOwnershipStore + GitLeaseStore, so SessionOwnershipRegistry's FSM
|
|
91
|
+
* is unchanged regardless of substrate. Synchronous, so within this process a stale
|
|
92
|
+
* competing candidate (computed from the same `current`) still loses.
|
|
93
|
+
*/
|
|
94
|
+
casWrite(candidate) {
|
|
95
|
+
const current = this.loadOne(candidate.sessionKey);
|
|
96
|
+
const curEpoch = current?.ownershipEpoch ?? 0;
|
|
97
|
+
if (candidate.ownershipEpoch > curEpoch) {
|
|
98
|
+
this.persist(candidate);
|
|
99
|
+
return { ok: true, observed: candidate };
|
|
100
|
+
}
|
|
101
|
+
return { ok: false, observed: current };
|
|
102
|
+
}
|
|
103
|
+
/** Every known ownership record (cache + a one-time disk scan). */
|
|
104
|
+
all() {
|
|
105
|
+
if (!this.scanned) {
|
|
106
|
+
try {
|
|
107
|
+
if (fs.existsSync(this.d.dir)) {
|
|
108
|
+
for (const f of fs.readdirSync(this.d.dir)) {
|
|
109
|
+
if (!f.endsWith('.json'))
|
|
110
|
+
continue;
|
|
111
|
+
try {
|
|
112
|
+
const rec = JSON.parse(fs.readFileSync(path.join(this.d.dir, f), 'utf-8'));
|
|
113
|
+
if (rec && typeof rec.ownershipEpoch === 'number' && typeof rec.sessionKey === 'string') {
|
|
114
|
+
// Cache only if fresher than (or absent in) the in-memory copy.
|
|
115
|
+
const existing = this.cache.get(rec.sessionKey);
|
|
116
|
+
if (!existing || rec.ownershipEpoch >= existing.ownershipEpoch)
|
|
117
|
+
this.cache.set(rec.sessionKey, rec);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch { /* @silent-fallback-ok — skip a corrupt single file in the scan; others still load */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch { /* @silent-fallback-ok — an unreadable dir yields the cache-only view, never a throw */ }
|
|
125
|
+
this.scanned = true;
|
|
126
|
+
}
|
|
127
|
+
return [...this.cache.values()];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=LocalSessionOwnershipStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalSessionOwnershipStore.js","sourceRoot":"","sources":["../../src/core/LocalSessionOwnershipStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OwnershipApplier — the cross-machine half of the transfer fix (spec
|
|
3
|
+
* docs/specs/live-user-channel-proof-standard.md §7.2). It runs OFF the routing
|
|
4
|
+
* hot path (on a periodic tick) and materializes durable local ownership records
|
|
5
|
+
* from the REPLICATED coherence-journal placement entries, so the machine a topic
|
|
6
|
+
* was transferred TO actually resolves itself (or the true owner) as the owner on
|
|
7
|
+
* the next inbound message.
|
|
8
|
+
*
|
|
9
|
+
* The bug it closes (2026-06-15): /pool/transfer wrote the target's ownership into
|
|
10
|
+
* the SOURCE machine's in-memory Map and emitted a placement entry to the journal.
|
|
11
|
+
* The journal entry REPLICATES to peers (peers/<machineId>.topic-placement.jsonl),
|
|
12
|
+
* but no one ever turned a replicated placement entry back into an ownership record
|
|
13
|
+
* on the receiving machine — so the target's owner-resolution read null and the
|
|
14
|
+
* router re-placed the topic locally instead of forwarding. This applier is exactly
|
|
15
|
+
* that missing step.
|
|
16
|
+
*
|
|
17
|
+
* Correctness model: a placement entry carries (topic, owner, ownershipEpoch). The
|
|
18
|
+
* applier takes the HIGHEST-epoch placement per topic across all streams (own +
|
|
19
|
+
* peer replicas) and adopts it into the local durable store via the store's
|
|
20
|
+
* fast-forward CAS — which lands ONLY if it strictly advances the local epoch. So
|
|
21
|
+
* a stale replicated entry can never clobber a fresher local decision, and the
|
|
22
|
+
* (topic, epoch) ordering is the same key the journal dedupes on. This adopts an
|
|
23
|
+
* ALREADY-DECIDED ownership (a replication step), not a new FSM transition — hence
|
|
24
|
+
* it writes the record directly rather than running place/claim actions.
|
|
25
|
+
*
|
|
26
|
+
* Split-brain: the applier does NOT independently re-solve lease split-brain (the
|
|
27
|
+
* spec's narrowed claim). It adopts the highest-epoch placement; under a healthy
|
|
28
|
+
* single-holder lease that is correct, and a genuine lease split-brain surfaces via
|
|
29
|
+
* the existing lease-attention path. Lease-epoch fencing inside the placement entry
|
|
30
|
+
* is a tracked follow-on (PlacementData does not yet carry leaseEpoch).
|
|
31
|
+
*/
|
|
32
|
+
import type { SessionOwnershipStore } from './SessionOwnershipRegistry.js';
|
|
33
|
+
/** The minimal slice of CoherenceJournalReader the applier needs (for testability). */
|
|
34
|
+
export interface PlacementReader {
|
|
35
|
+
query(opts: {
|
|
36
|
+
kind?: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
topic?: number;
|
|
39
|
+
}): {
|
|
40
|
+
entries: Array<{
|
|
41
|
+
topic?: number;
|
|
42
|
+
machine: string;
|
|
43
|
+
data: Record<string, unknown>;
|
|
44
|
+
source?: string;
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export interface OwnershipApplierDeps {
|
|
49
|
+
/** Reads replicated + own placement entries (CoherenceJournalReader). */
|
|
50
|
+
reader: PlacementReader;
|
|
51
|
+
/** The SAME durable store the SessionOwnershipRegistry reads — so a materialized
|
|
52
|
+
* record is immediately visible to owner-resolution / routing. */
|
|
53
|
+
store: SessionOwnershipStore;
|
|
54
|
+
selfMachineId: string;
|
|
55
|
+
/** Max placement entries to scan per tick (bounded cost). Default 1000. */
|
|
56
|
+
scanLimit?: number;
|
|
57
|
+
logger?: (msg: string) => void;
|
|
58
|
+
now?: () => number;
|
|
59
|
+
}
|
|
60
|
+
export interface OwnershipApplyResult {
|
|
61
|
+
/** Distinct topics that had a placement entry this tick. */
|
|
62
|
+
examined: number;
|
|
63
|
+
/** Topics whose local ownership record was advanced from a (newer) placement. */
|
|
64
|
+
materialized: number;
|
|
65
|
+
}
|
|
66
|
+
export declare class OwnershipApplier {
|
|
67
|
+
private readonly d;
|
|
68
|
+
constructor(deps: OwnershipApplierDeps);
|
|
69
|
+
private now;
|
|
70
|
+
private log;
|
|
71
|
+
/**
|
|
72
|
+
* One pass: adopt the highest-epoch placement per topic into the local store.
|
|
73
|
+
* Returns counts for observability. Never throws — a read/parse failure degrades
|
|
74
|
+
* to "materialized fewer this tick", never a crash on the routing-adjacent path.
|
|
75
|
+
*/
|
|
76
|
+
tick(): OwnershipApplyResult;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=OwnershipApplier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OwnershipApplier.d.ts","sourceRoot":"","sources":["../../src/core/OwnershipApplier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAE3E,uFAAuF;AACvF,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QAC9D,OAAO,EAAE,KAAK,CAAC;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACrG,CAAC;CACH;AAED,MAAM,WAAW,oBAAoB;IACnC,yEAAyE;IACzE,MAAM,EAAE,eAAe,CAAC;IACxB;uEACmE;IACnE,KAAK,EAAE,qBAAqB,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAuB;gBAE7B,IAAI,EAAE,oBAAoB;IAItC,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,GAAG;IAIX;;;;OAIG;IACH,IAAI,IAAI,oBAAoB;CAkD7B"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OwnershipApplier — the cross-machine half of the transfer fix (spec
|
|
3
|
+
* docs/specs/live-user-channel-proof-standard.md §7.2). It runs OFF the routing
|
|
4
|
+
* hot path (on a periodic tick) and materializes durable local ownership records
|
|
5
|
+
* from the REPLICATED coherence-journal placement entries, so the machine a topic
|
|
6
|
+
* was transferred TO actually resolves itself (or the true owner) as the owner on
|
|
7
|
+
* the next inbound message.
|
|
8
|
+
*
|
|
9
|
+
* The bug it closes (2026-06-15): /pool/transfer wrote the target's ownership into
|
|
10
|
+
* the SOURCE machine's in-memory Map and emitted a placement entry to the journal.
|
|
11
|
+
* The journal entry REPLICATES to peers (peers/<machineId>.topic-placement.jsonl),
|
|
12
|
+
* but no one ever turned a replicated placement entry back into an ownership record
|
|
13
|
+
* on the receiving machine — so the target's owner-resolution read null and the
|
|
14
|
+
* router re-placed the topic locally instead of forwarding. This applier is exactly
|
|
15
|
+
* that missing step.
|
|
16
|
+
*
|
|
17
|
+
* Correctness model: a placement entry carries (topic, owner, ownershipEpoch). The
|
|
18
|
+
* applier takes the HIGHEST-epoch placement per topic across all streams (own +
|
|
19
|
+
* peer replicas) and adopts it into the local durable store via the store's
|
|
20
|
+
* fast-forward CAS — which lands ONLY if it strictly advances the local epoch. So
|
|
21
|
+
* a stale replicated entry can never clobber a fresher local decision, and the
|
|
22
|
+
* (topic, epoch) ordering is the same key the journal dedupes on. This adopts an
|
|
23
|
+
* ALREADY-DECIDED ownership (a replication step), not a new FSM transition — hence
|
|
24
|
+
* it writes the record directly rather than running place/claim actions.
|
|
25
|
+
*
|
|
26
|
+
* Split-brain: the applier does NOT independently re-solve lease split-brain (the
|
|
27
|
+
* spec's narrowed claim). It adopts the highest-epoch placement; under a healthy
|
|
28
|
+
* single-holder lease that is correct, and a genuine lease split-brain surfaces via
|
|
29
|
+
* the existing lease-attention path. Lease-epoch fencing inside the placement entry
|
|
30
|
+
* is a tracked follow-on (PlacementData does not yet carry leaseEpoch).
|
|
31
|
+
*/
|
|
32
|
+
export class OwnershipApplier {
|
|
33
|
+
d;
|
|
34
|
+
constructor(deps) {
|
|
35
|
+
this.d = deps;
|
|
36
|
+
}
|
|
37
|
+
now() {
|
|
38
|
+
return (this.d.now ?? Date.now)();
|
|
39
|
+
}
|
|
40
|
+
log(m) {
|
|
41
|
+
this.d.logger?.(`[ownership-applier] ${m}`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One pass: adopt the highest-epoch placement per topic into the local store.
|
|
45
|
+
* Returns counts for observability. Never throws — a read/parse failure degrades
|
|
46
|
+
* to "materialized fewer this tick", never a crash on the routing-adjacent path.
|
|
47
|
+
*/
|
|
48
|
+
tick() {
|
|
49
|
+
let examined = 0;
|
|
50
|
+
let materialized = 0;
|
|
51
|
+
try {
|
|
52
|
+
const res = this.d.reader.query({ kind: 'topic-placement', limit: this.d.scanLimit ?? 1000 });
|
|
53
|
+
// Collapse to the highest-epoch placement per topic across own + replica streams.
|
|
54
|
+
const bestByTopic = new Map();
|
|
55
|
+
for (const e of res.entries) {
|
|
56
|
+
if (e.topic == null)
|
|
57
|
+
continue;
|
|
58
|
+
const owner = typeof e.data.owner === 'string' ? e.data.owner : '';
|
|
59
|
+
const epoch = typeof e.data.epoch === 'number' ? e.data.epoch : Number(e.data.epoch);
|
|
60
|
+
if (!owner || !Number.isFinite(epoch) || epoch <= 0)
|
|
61
|
+
continue;
|
|
62
|
+
const key = String(e.topic);
|
|
63
|
+
const cur = bestByTopic.get(key);
|
|
64
|
+
if (!cur || epoch > cur.epoch)
|
|
65
|
+
bestByTopic.set(key, { owner, epoch });
|
|
66
|
+
}
|
|
67
|
+
examined = bestByTopic.size;
|
|
68
|
+
for (const [sessionKey, best] of bestByTopic) {
|
|
69
|
+
const local = this.d.store.read(sessionKey);
|
|
70
|
+
const localEpoch = local?.ownershipEpoch ?? 0;
|
|
71
|
+
if (best.epoch <= localEpoch)
|
|
72
|
+
continue; // not newer → nothing to adopt (fast-forward only)
|
|
73
|
+
const now = this.now();
|
|
74
|
+
const rec = {
|
|
75
|
+
sessionKey,
|
|
76
|
+
ownerMachineId: best.owner,
|
|
77
|
+
ownershipEpoch: best.epoch,
|
|
78
|
+
status: 'active',
|
|
79
|
+
// Adoption nonce keyed on (owner, epoch) — the same ordering identity the
|
|
80
|
+
// journal uses; never a local action nonce.
|
|
81
|
+
nonce: `applier:${best.owner}:${best.epoch}`,
|
|
82
|
+
timestamp: now,
|
|
83
|
+
updatedAt: new Date(now).toISOString(),
|
|
84
|
+
};
|
|
85
|
+
const r = this.d.store.casWrite(rec);
|
|
86
|
+
if (r.ok) {
|
|
87
|
+
materialized++;
|
|
88
|
+
this.log(`materialized topic ${sessionKey} → owner ${best.owner} @epoch ${best.epoch}` +
|
|
89
|
+
(best.owner === this.d.selfMachineId ? ' (SELF — this machine now serves it)' : ' (peer — route forwards there)'));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
// NOT silent: logged. The applier is best-effort replication; a failure means
|
|
95
|
+
// this tick adopted fewer records — the next tick (or the reconciler) converges.
|
|
96
|
+
this.log(`tick failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
97
|
+
}
|
|
98
|
+
return { examined, materialized };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=OwnershipApplier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OwnershipApplier.js","sourceRoot":"","sources":["../../src/core/OwnershipApplier.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostUpdateMigrator.d.ts","sourceRoot":"","sources":["../../src/core/PostUpdateMigrator.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"PostUpdateMigrator.d.ts","sourceRoot":"","sources":["../../src/core/PostUpdateMigrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -3749,6 +3749,15 @@ setTimeout(() => process.exit(0), 2000);
|
|
|
3749
3749
|
patched = true;
|
|
3750
3750
|
result.upgraded.push('CLAUDE.md: added Self-Unblock Before Escalating section');
|
|
3751
3751
|
}
|
|
3752
|
+
// Live-User-Channel Proof Before Done (docs/specs/live-user-channel-proof-standard.md,
|
|
3753
|
+
// CMT-1568) — constitutional standard. Migration Parity item 3 + Agent Awareness:
|
|
3754
|
+
// existing agents learn the "not done until proven live through the real channel"
|
|
3755
|
+
// bar via this appended section (same text as generateClaudeMd). Content-sniffed.
|
|
3756
|
+
if (!content.includes('Live-User-Channel Proof Before Done')) {
|
|
3757
|
+
content += `\n**Live-User-Channel Proof Before Done** — A user-facing feature is NOT "done" until a user-role session has driven it end-to-end through its REAL user surface — Telegram AND Slack for a channel feature, the real dashboard for a dashboard feature — across the required risk categories (happy-path, channel-parity, lifecycle, permission/volatile, failure/rollback, concurrency, idempotency, regression), in a LIVE environment, BEFORE the operator is ever asked to test. The operator discovering a defect on first use is a process failure. Before claiming done/shipped on a user-facing feature I run the user-role live-test harness (acts as the user through the real surface, records a signed PASS/FAIL scenario matrix; volatile/permission scenarios run on throwaway agents + demo channels, never the live operator channel); the completion gate refuses "done" without that artifact, and the north-star metric is *operator-found escapes* (a defect you hit after the gate passed) driven toward zero. Spec: \`docs/specs/live-user-channel-proof-standard.md\`. Constitution: "Live-User-Channel Proof Before Done".\n`;
|
|
3758
|
+
patched = true;
|
|
3759
|
+
result.upgraded.push('CLAUDE.md: added Live-User-Channel Proof Before Done section');
|
|
3760
|
+
}
|
|
3752
3761
|
// Action-Claim Follow-Through Sentinel (action-claim-followthrough-sentinel.md).
|
|
3753
3762
|
// Agent Awareness: an agent that doesn't know this exists will be confused when a
|
|
3754
3763
|
// commitment appears after it says "I'll restart X". Content-sniffed; idempotent.
|