@astrosheep/square 0.3.86 → 0.3.88

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 (143) hide show
  1. package/dist/artifact.d.ts +6 -1
  2. package/dist/artifact.js +79 -10
  3. package/dist/boundary-presentation.d.ts +5 -4
  4. package/dist/boundary-presentation.js +21 -25
  5. package/dist/catch-decisions.js +2 -1
  6. package/dist/claude-delivery-status.d.ts +8 -2
  7. package/dist/claude-delivery-status.js +26 -10
  8. package/dist/claude-delivery.d.ts +8 -4
  9. package/dist/claude-delivery.js +114 -104
  10. package/dist/claude-mcp-context.js +2 -2
  11. package/dist/claude-mod.js +119 -105
  12. package/dist/cli/context.d.ts +3 -1
  13. package/dist/cli/context.js +25 -1
  14. package/dist/cli/express-presentation.d.ts +3 -1
  15. package/dist/cli/express-presentation.js +14 -10
  16. package/dist/cli/mcp-server-command.js +2 -2
  17. package/dist/cli/observation-commands.js +1 -0
  18. package/dist/cli/registry.js +0 -2
  19. package/dist/cli/square-commands.d.ts +4 -2
  20. package/dist/cli/square-commands.js +21 -6
  21. package/dist/codex-hook.d.ts +3 -3
  22. package/dist/codex-hook.js +4 -4
  23. package/dist/codex-queue.d.ts +5 -6
  24. package/dist/codex-queue.js +9 -5
  25. package/dist/decisions.d.ts +3 -2
  26. package/dist/decisions.js +18 -6
  27. package/dist/delivery-health.d.ts +32 -2
  28. package/dist/delivery-health.js +108 -45
  29. package/dist/delivery-operations.d.ts +1 -2
  30. package/dist/delivery-operations.js +155 -196
  31. package/dist/delivery.d.ts +14 -23
  32. package/dist/delivery.js +48 -18
  33. package/dist/file-lock.d.ts +8 -1
  34. package/dist/file-lock.js +2 -0
  35. package/dist/help.js +8 -11
  36. package/dist/inbox.d.ts +35 -9
  37. package/dist/inbox.js +228 -144
  38. package/dist/index.d.ts +3 -4
  39. package/dist/index.js +1 -3
  40. package/dist/list.js +9 -4
  41. package/dist/mcp.js +12 -1
  42. package/dist/model.d.ts +39 -5
  43. package/dist/model.js +84 -0
  44. package/dist/notifications.d.ts +8 -6
  45. package/dist/notifications.js +56 -143
  46. package/dist/operation-presentation.js +7 -4
  47. package/dist/participant-identity.d.ts +0 -3
  48. package/dist/participant-identity.js +0 -31
  49. package/dist/paseo-delivery.d.ts +5 -6
  50. package/dist/paseo-delivery.js +13 -8
  51. package/dist/paseo.d.ts +1 -1
  52. package/dist/paseo.js +1 -1
  53. package/dist/plugins/opencode/delivery.d.ts +8 -3
  54. package/dist/plugins/opencode/delivery.js +187 -132
  55. package/dist/plugins/opencode/index.js +12 -38
  56. package/dist/ports.d.ts +112 -40
  57. package/dist/presentation-operations.d.ts +1 -1
  58. package/dist/presentation-operations.js +51 -35
  59. package/dist/presentation.js +8 -3
  60. package/dist/seats.d.ts +7 -0
  61. package/dist/seats.js +27 -0
  62. package/dist/session-lifecycle.d.ts +2 -2
  63. package/dist/session-lifecycle.js +10 -12
  64. package/dist/sqlite-square.d.ts +20 -9
  65. package/dist/sqlite-square.js +476 -341
  66. package/dist/square-api.d.ts +1 -0
  67. package/dist/square-api.js +3 -4
  68. package/dist/square-core.d.ts +3 -2
  69. package/dist/square-domain.d.ts +12 -5
  70. package/dist/square-domain.js +163 -7
  71. package/dist/square-facade.d.ts +16 -10
  72. package/dist/square-index.d.ts +24 -0
  73. package/dist/square-index.js +131 -0
  74. package/dist/square-legacy-framing.d.ts +9 -0
  75. package/dist/square-legacy-framing.js +56 -0
  76. package/dist/square-migration-host-ledger.d.ts +26 -0
  77. package/dist/square-migration-host-ledger.js +168 -0
  78. package/dist/square-migration.d.ts +5 -5
  79. package/dist/square-migration.js +373 -71
  80. package/dist/square-sqlite.d.ts +9 -9
  81. package/dist/square-sqlite.js +329 -79
  82. package/dist/square-stdio.d.ts +1 -1
  83. package/dist/square-stdio.js +12 -7
  84. package/dist/square-tools.d.ts +1 -1
  85. package/dist/square-tools.js +80 -27
  86. package/dist/stdio-server.js +4 -2
  87. package/dist/stream.d.ts +3 -2
  88. package/dist/stream.js +9 -10
  89. package/dist/views.d.ts +9 -14
  90. package/dist/views.js +52 -70
  91. package/dist/wakes.d.ts +6 -5
  92. package/dist/wakes.js +20 -17
  93. package/guides/application-facade-delivery-evidence.md +1 -1
  94. package/guides/claude-native.md +9 -9
  95. package/guides/mcp.md +2 -2
  96. package/guides/migration.md +107 -67
  97. package/guides/pi-tools.md +1 -1
  98. package/guides/row-storage-design.md +1 -1
  99. package/guides/square-api.md +61 -28
  100. package/package.json +1 -1
  101. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  102. package/plugins/claude/hooks/register.js +1 -1
  103. package/plugins/claude/skills/square/SKILL.md +4 -4
  104. package/plugins/claude/types.d.ts +1 -1
  105. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  106. package/plugins/opencode/delivery.ts +170 -118
  107. package/plugins/opencode/index.ts +12 -30
  108. package/plugins/pi/square-pi.js +56 -18
  109. package/skills/square/SKILL.md +4 -4
  110. package/dist/cli/ledger-command.d.ts +0 -12
  111. package/dist/cli/ledger-command.js +0 -72
  112. package/dist/host-ledger-file-adapter.d.ts +0 -47
  113. package/dist/host-ledger-file-adapter.js +0 -384
  114. package/dist/host-ledger-migration.d.ts +0 -27
  115. package/dist/host-ledger-migration.js +0 -264
  116. package/dist/host-ledger-root.d.ts +0 -2
  117. package/dist/host-ledger-root.js +0 -9
  118. package/dist/host-ledger-sqlite.d.ts +0 -65
  119. package/dist/host-ledger-sqlite.js +0 -368
  120. package/dist/host-ledger.d.ts +0 -195
  121. package/dist/host-ledger.js +0 -1
  122. package/dist/ledger-maintenance.d.ts +0 -30
  123. package/dist/ledger-maintenance.js +0 -110
  124. package/dist/open-square.d.ts +0 -38
  125. package/dist/open-square.js +0 -81
  126. package/dist/participant-host.d.ts +0 -23
  127. package/dist/participant-host.js +0 -92
  128. package/dist/registry.d.ts +0 -55
  129. package/dist/registry.js +0 -123
  130. package/dist/routes.d.ts +0 -39
  131. package/dist/routes.js +0 -99
  132. package/dist/square-actions.d.ts +0 -56
  133. package/dist/square-actions.js +0 -353
  134. package/dist/square-projections.d.ts +0 -31
  135. package/dist/square-projections.js +0 -64
  136. package/dist/wake-attempts.d.ts +0 -10
  137. package/dist/wake-attempts.js +0 -39
  138. package/dist/wake-eligibility.d.ts +0 -53
  139. package/dist/wake-eligibility.js +0 -123
  140. package/dist/wake-evidence.d.ts +0 -33
  141. package/dist/wake-evidence.js +0 -32
  142. package/guides/host-ledger.md +0 -58
  143. package/guides/indexed-host-ledger-performance.md +0 -39
@@ -1,30 +1,40 @@
1
1
  import { stat } from 'node:fs/promises';
2
- import { randomUUID } from 'node:crypto';
3
- import { openSquare } from './open-square.js';
4
- import { closeOpenSquare } from './open-square.js';
5
- import { hostLedgerForEnv } from './registry.js';
6
- import { projectPresentationEvidence, presentationSuppressesWake } from './square-projections.js';
7
- import { nativePendingPreview } from './inbox.js';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { createArtifactPort } from './artifact.js';
8
4
  import { presentPending } from './presentation-operations.js';
9
- import { canonicalPath } from './canonical-path.js';
5
+ import { projectSessionCandidates, nativePendingPreview } from './inbox.js';
6
+ import { sameBody } from './model.js';
10
7
  import { parseActivityId } from './square-core.js';
8
+ import { asSeatRef } from './seats.js';
11
9
  import { writeClaudeNative } from './packages/agent-delivery/src/claude-native.js';
12
10
  const marker = /(?:^|\n)\[square-inbox:([a-zA-Z0-9-]+)\](?:\n|$)/g;
13
11
  export function nativeTokens(text) { return [...text.matchAll(marker)].map((match) => match[1]); }
14
12
  export function nativePayload(token, preview) { return `[square-inbox:${token}]\n${preview}\n[/square-inbox:${token}]`; }
15
13
  function nativeSupported(route) { return route.address.loaded === 'true' && !!route.address.endpoint; }
16
- /**
17
- * The shared entry owns platform policy, token resolution and frame shape; Square
18
- * owns the payload, the correlation and the evidence. A local write is never proof
19
- * of native admission.
20
- */
14
+ function sameAddress(left, right) {
15
+ if (left === undefined)
16
+ return false;
17
+ const keys = Object.keys(left);
18
+ return keys.length === Object.keys(right).length && keys.every((key) => left[key] === right[key]);
19
+ }
20
+ function hashPayload(payload) { return createHash('sha256').update(payload).digest('hex'); }
21
+ function previewFacts(facts, request) {
22
+ const index = parseActivityId(request.activity);
23
+ if (index === undefined || request.route.kind !== 'claude-native' || request.route.address.endpoint === undefined)
24
+ return undefined;
25
+ return nativePendingPreview(facts, { location: request.location, routeKind: 'claude-native', sessionId: request.route.sessionId,
26
+ participant: request.participant, actIndex: index, address: { endpoint: request.route.address.endpoint }, now: Date.now() });
27
+ }
28
+ async function preview(artifact, request) {
29
+ return (await artifact.read((facts) => previewFacts(facts, request))).value;
30
+ }
21
31
  async function writeClaudeWake(sessionId, endpoint, messageId, payload, timeoutMs, signal) {
22
32
  const result = await writeClaudeNative({ sessionId, endpoint, messageId }, payload, { deadline: Date.now() + timeoutMs, signal });
23
33
  if (result.outcome === 'unavailable')
24
34
  return { outcome: 'failed', unavailable: true, retainRoute: true, signature: 'claude_endpoint_unavailable', message: result.message };
25
35
  return { outcome: 'unknown', signature: 'native_admission_unconfirmed', message: result.outcome === 'written' ? 'Claude accepted the local write; native admission is unconfirmed.' : result.message };
26
36
  }
27
- export function createClaudeWakeTransport(hostLedger, env = process.env) {
37
+ export function createClaudeWakeTransport(env = process.env) {
28
38
  return {
29
39
  async probe(route) {
30
40
  if (!nativeSupported(route))
@@ -36,108 +46,108 @@ export function createClaudeWakeTransport(hostLedger, env = process.env) {
36
46
  return { outcome: 'not-capable', diagnostic: 'Claude native inbox endpoint unavailable.' };
37
47
  }
38
48
  },
39
- attempt: (request, timeoutMs, beforeSend) => dispatchClaude(request, hostLedger, timeoutMs, beforeSend, env),
49
+ attempt: (request, timeoutMs, beforeSend) => dispatchClaude(request, timeoutMs, beforeSend, env),
40
50
  };
41
51
  }
42
- async function currentDelivery(row, hostLedger) {
43
- if (row.nativeDelivery?.harness !== 'claude')
52
+ async function dispatchClaude(request, timeoutMs, beforeSend, env = process.env) {
53
+ void env;
54
+ const signal = AbortSignal.timeout(timeoutMs);
55
+ if (!nativeSupported(request.route) || request.artifact === undefined || request.claimToken === undefined)
56
+ return { outcome: 'not-capable', diagnostic: 'Claude native delivery needs a current artifact claim and loaded inbox.' };
57
+ if (signal.aborted || !await beforeSend())
58
+ return { outcome: 'unknown', signature: 'native_send_suppressed' };
59
+ const prepared = await preview(request.artifact, request);
60
+ if (prepared === undefined)
61
+ return { outcome: 'not-capable', diagnostic: 'Claude activity is no longer current.' };
62
+ const messageId = randomUUID();
63
+ const payload = nativePayload(request.claimToken, prepared.payload);
64
+ const correlation = { harness: 'claude', token: request.claimToken, messageId, payloadHash: hashPayload(payload) };
65
+ if (!await request.artifact.prepareDelivery({ seat: request.seat, claimToken: request.claimToken, correlation }))
66
+ return { outcome: 'not-capable', diagnostic: 'Claude delivery claim is no longer current.' };
67
+ const fresh = await preview(request.artifact, request);
68
+ if (fresh === undefined || nativePayload(request.claimToken, fresh.payload) !== payload)
69
+ return { outcome: 'not-capable', diagnostic: 'Claude activity changed before native write.' };
70
+ if (signal.aborted || !await beforeSend())
71
+ return { outcome: 'not-capable', diagnostic: 'Claude activity was consumed before native write.' };
72
+ return writeClaudeWake(request.route.sessionId, request.route.address.endpoint, messageId, payload, timeoutMs, signal);
73
+ }
74
+ function currentPayload(facts, request) {
75
+ const seat = facts.seat(request.participant), evidence = facts.deliveryByToken(request.claimToken);
76
+ if (seat === undefined || evidence === undefined || seat.generation !== evidence.generation
77
+ || seat.generation !== request.seat.generation || !sameBody(seat.body, request.seat.body)
78
+ || seat.body.channel !== 'claude-code' || seat.body.session !== request.route.sessionId
79
+ || evidence.activity !== request.activity || !['dispatching', 'unknown', 'accepted'].includes(evidence.outcome)
80
+ || seat.wakeAddress?.kind !== 'claude-native' || !sameAddress(seat.wakeAddress.address, request.route.address))
44
81
  return undefined;
45
- const native = row.nativeDelivery;
46
- const owner = (await hostLedger.listPresence({ location: row.location, participant: row.participant })).find((binding) => binding.session === row.session && binding.epoch === native.epoch);
47
- const index = parseActivityId(row.activity);
48
- if (!owner || index === undefined || !row.claimToken)
82
+ const native = previewFacts(facts, request);
83
+ if (native === undefined)
49
84
  return undefined;
50
- const square = await openSquare(owner.location, { hostLedger });
51
- try {
52
- const { value: preview } = await square.artifact.read((facts) => nativePendingPreview(facts, { location: row.location, routeKind: 'claude-native', sessionId: row.session, participant: row.participant, actIndex: index, epoch: native.epoch, address: { endpoint: native.endpoint }, cancelledThrough: owner.cancelledThrough, now: Date.now() }));
53
- if (!preview || nativePayload(row.claimToken, preview.payload) !== native.payload)
54
- return undefined;
55
- return { cancelledThrough: owner.cancelledThrough, clipped: preview.clipped };
56
- }
57
- finally {
58
- await closeOpenSquare(square);
59
- }
85
+ const payload = nativePayload(request.claimToken, native.payload);
86
+ return evidence.correlation?.payloadHash === hashPayload(payload) ? payload : undefined;
60
87
  }
61
- /** Square owns rendering, correlation and evidence; socket transport owns none of them. */
62
- async function dispatchClaude(request, hostLedger, timeoutMs, beforeSend, env = process.env) {
63
- request = { ...request, location: await canonicalPath(request.location) };
64
- const signal = AbortSignal.timeout(timeoutMs);
65
- if (!nativeSupported(request.route))
66
- return { outcome: 'not-capable', diagnostic: 'Claude native inbox requires a loaded interactive Square mod.' };
67
- return hostLedger.withClaimLock(async () => {
68
- if (signal.aborted)
69
- return { outcome: 'not-capable', diagnostic: 'Native dispatch deadline elapsed before send.' };
70
- if (!request.claimToken || !await (beforeSend?.() ?? Promise.resolve(true)))
71
- return { outcome: 'unknown', signature: 'native_send_suppressed' };
72
- const owner = (await hostLedger.listPresence({ location: request.location, participant: request.participant })).find((binding) => binding.session === request.route.sessionId && binding.epoch === request.route.epoch);
73
- if (!owner)
74
- return { outcome: 'not-capable', diagnostic: 'Claude binding changed.' };
75
- const square = await openSquare(request.location, { hostLedger });
76
- let payload;
88
+ async function rowsForSession(sessionId, env, tokens) {
89
+ const rows = [];
90
+ const paths = new Set((await projectSessionCandidates({ sessionId, channel: 'claude-code', env })).map(candidate => candidate.location));
91
+ for (const location of paths) {
92
+ const artifact = createArtifactPort(location);
77
93
  try {
78
- const index = parseActivityId(request.activity);
79
- const preview = index === undefined ? undefined : (await square.artifact.read((facts) => nativePendingPreview(facts, { location: request.location, routeKind: 'claude-native', sessionId: request.route.sessionId, participant: request.participant, actIndex: index, epoch: request.route.epoch, address: { endpoint: request.route.address.endpoint }, cancelledThrough: owner.cancelledThrough, now: Date.now() }))).value;
80
- if (!preview)
81
- return { outcome: 'not-capable', diagnostic: 'Catch or consumption owns this activity.' };
82
- payload = nativePayload(request.claimToken, preview.payload);
94
+ const found = await artifact.read((facts) => tokens.flatMap((token) => {
95
+ const evidence = facts.deliveryByToken(token);
96
+ if (evidence?.kind !== 'wake' || evidence.correlation?.harness !== 'claude' || evidence.correlation.token !== token
97
+ || evidence.route?.kind !== 'claude-native' || evidence.route.address.sessionId !== sessionId)
98
+ return [];
99
+ const row = { artifact, evidence };
100
+ const seat = facts.seat(evidence.participant), index = parseActivityId(evidence.activity);
101
+ const item = index === undefined ? undefined : facts.activity(index);
102
+ if (seat === undefined || item?.kind !== 'say')
103
+ return [row];
104
+ const route = { location, participant: seat.participant, sessionId, channel: 'claude-code',
105
+ ...evidence.route, updatedAt: evidence.at };
106
+ const request = { location, participant: seat.participant, activity: evidence.activity,
107
+ actor: item.actor, seat: asSeatRef(seat), claimToken: token, attemptN: evidence.attemptN, route, artifact };
108
+ return [{ ...row, request, payload: currentPayload(facts, request) }];
109
+ }));
110
+ rows.push(...found.value);
111
+ if (found.value.length === 0)
112
+ await artifact.close();
83
113
  }
84
- finally {
85
- await closeOpenSquare(square);
114
+ catch {
115
+ await artifact.close();
86
116
  }
87
- if (!await (beforeSend?.() ?? Promise.resolve(true)))
88
- return { outcome: 'not-capable', diagnostic: 'Native activity was consumed before send.' };
89
- const native = { harness: 'claude', payload, epoch: owner.epoch ?? 0, endpoint: request.route.address.endpoint, messageId: randomUUID() };
90
- const row = { location: request.location, participant: request.participant, session: request.route.sessionId, activity: request.activity, kind: 'wake', outcome: 'dispatching', routeKind: 'claude-native', attemptN: request.attemptN, claimToken: request.claimToken, nativeDelivery: native };
91
- if (!await currentDelivery(row, hostLedger) || !await hostLedger.prepareNativeWake(row))
92
- return { outcome: 'not-capable', diagnostic: 'Native delivery is no longer current.' };
93
- if (!await (beforeSend?.() ?? Promise.resolve(true)) || !await currentDelivery(row, hostLedger))
94
- return { outcome: 'not-capable', diagnostic: 'Catch, cancellation or consumption won before native write.' };
95
- return writeClaudeWake(row.session, native.endpoint, native.messageId, payload, timeoutMs, signal);
96
- }, signal);
117
+ }
118
+ return rows;
97
119
  }
98
- /** Retired tokens veto a frame; only live correlations and bound memberships confer authority. */
120
+ /** Recognize the whole frame first; one inactive token vetoes it before any evidence/receipt write. */
99
121
  export async function observeClaudeDelivery(sessionId, text, operation, env = process.env) {
100
- const ledger = hostLedgerForEnv(env);
101
- return ledger.withClaimLock(async () => {
102
- const tokens = nativeTokens(text);
103
- const { deliveries: rows, retired } = await ledger.lookupNativeTokens({ harness: 'claude', session: sessionId, tokens });
104
- // A stale token vetoes the entire frame, including a mixed current/stale frame,
105
- // before admission, artifact reads or presentation can confer any authority.
106
- if (retired.length)
107
- return { recognized: true, current: false };
108
- let current = false;
122
+ const tokens = nativeTokens(text);
123
+ if (tokens.length === 0)
124
+ return { recognized: false, current: false };
125
+ const rows = await rowsForSession(sessionId, env, [...new Set(tokens)]);
126
+ const recognized = rows.length > 0;
127
+ const frames = [...text.matchAll(/(?:^|\n)(\[square-inbox:([a-zA-Z0-9-]+)\]\n[\s\S]*?\n\[\/square-inbox:\2\])(?=\n|$)/g)];
128
+ try {
129
+ if (!recognized || rows.some(row => {
130
+ const matching = frames.filter(frame => frame[2] === row.evidence.claimToken);
131
+ return row.payload === undefined || matching.length !== tokens.filter(token => token === row.evidence.claimToken).length
132
+ || matching.some(frame => frame[1] !== row.payload);
133
+ }))
134
+ return { recognized, current: false };
109
135
  for (const row of rows) {
110
- if (row.nativeDelivery?.harness !== 'claude' || !['dispatching', 'unknown', 'accepted'].includes(row.outcome))
136
+ if (operation === 'guard')
111
137
  continue;
112
- const native = row.nativeDelivery;
113
- const pending = await currentDelivery(row, ledger).catch(() => undefined);
114
- if (!pending || !text.includes(native.payload))
115
- continue;
116
- current = true;
117
- if (operation === 'admitted') {
118
- if (text === native.payload)
119
- await ledger.confirmWakeAdmission({ ...row, claimToken: row.claimToken });
120
- }
121
- if (operation === 'stored') {
122
- await ledger.confirmWakeAdmission({ ...row, claimToken: row.claimToken });
123
- const prior = await projectPresentationEvidence({ hostLedger: ledger, location: row.location, participant: row.participant, sessionId, activity: row.activity });
124
- if (presentationSuppressesWake(prior))
125
- continue;
126
- const square = await openSquare(row.location, { hostLedger: ledger, env });
127
- try {
128
- await presentPending({ artifact: square.artifact, hostLedger: ledger, location: row.location, participant: row.participant, session: sessionId, activity: row.activity, sink: { present() { } }, markSeen: !pending.clipped, current: (facts) => {
129
- const index = parseActivityId(row.activity);
130
- if (index === undefined)
131
- return false;
132
- const preview = nativePendingPreview(facts, { location: row.location, routeKind: 'claude-native', sessionId: row.session, participant: row.participant, actIndex: index, epoch: native.epoch, address: { endpoint: native.endpoint }, cancelledThrough: pending.cancelledThrough, now: Date.now() });
133
- return preview !== undefined && nativePayload(row.claimToken, preview.payload) === native.payload;
134
- } });
135
- }
136
- finally {
137
- await closeOpenSquare(square);
138
- }
139
- }
138
+ const request = row.request, payload = row.payload;
139
+ const admitted = await row.artifact.transactAs({ kind: 'participant', seat: request.seat }, facts => currentPayload(facts, request) === payload && facts.completeDelivery({ seat: request.seat,
140
+ activity: row.evidence.activity, claimToken: request.claimToken, outcome: 'accepted', at: Date.now() })).catch(() => ({ value: false }));
141
+ if (!admitted.value)
142
+ return { recognized, current: false };
143
+ if (operation === 'stored')
144
+ await presentPending({ artifact: row.artifact, location: request.location,
145
+ participant: request.participant, activity: request.activity, seat: request.seat, sink: { present() { } },
146
+ current: facts => currentPayload(facts, request) === payload });
140
147
  }
141
- return { recognized: rows.length > 0, current };
142
- });
148
+ return { recognized, current: true };
149
+ }
150
+ finally {
151
+ await Promise.all([...new Set(rows.map(row => row.artifact))].map(artifact => artifact.close()));
152
+ }
143
153
  }
@@ -3,7 +3,7 @@ import { promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { z } from 'zod/v4';
5
5
  import { withFileLock } from './file-lock.js';
6
- import { hostLedgerRoot } from './host-ledger-root.js';
6
+ import { squareIndexPath } from './square-index.js';
7
7
  import { SquareError } from './model.js';
8
8
  const LEASE_MS = 10 * 60 * 1000;
9
9
  const text = z.string().max(4096).refine(value => value.trim().length > 0 && !/[\x00-\x1f\x7f]/u.test(value));
@@ -12,7 +12,7 @@ const coordinate = z.object({
12
12
  });
13
13
  const record = coordinate.extend({ token: z.string().regex(/^[a-f0-9]{64}$/u), expiresAt: z.number() });
14
14
  const unavailable = () => new SquareError('unavailable', 'Claude MCP call context unavailable');
15
- const storeRoot = (env) => path.resolve(hostLedgerRoot(env), 'claude-mcp-calls');
15
+ const storeRoot = (env) => path.join(path.dirname(squareIndexPath(env)), 'claude-mcp-calls');
16
16
  const callFile = (root, id) => path.join(root, `${createHash('sha256').update(id).digest('hex')}.json`);
17
17
  async function readRecord(file) {
18
18
  try {
@@ -1,14 +1,18 @@
1
1
  import { operationEnv } from './participant-identity.js';
2
- import { claimSessionParticipant, hostLedgerForEnv } from './registry.js';
3
- import { openSquare } from './open-square.js';
4
- import { closeOpenSquare } from './open-square.js';
5
- import { publishWakeRoute, retireWakeRouteFromArtifact } from './routes.js';
2
+ import { createArtifactPort } from './artifact.js';
3
+ import { recordClaudeDeliveryStatus } from './claude-delivery-status.js';
6
4
  import { Square } from './square-api.js';
7
5
  import { observeClaudeDelivery } from './claude-delivery.js';
8
6
  import { processActNotificationsOnce } from './notifications.js';
9
7
  import { sweepPending } from './delivery-operations.js';
10
8
  import { beginClaudeMcpCall, endClaudeMcpCall } from './claude-mcp-context.js';
11
- import { recordClaudeDeliveryStatus } from './claude-delivery-status.js';
9
+ import { asSeatRef } from './seats.js';
10
+ import { projectSessionSeats, projectSessionCandidates } from './inbox.js';
11
+ import { InternalSquareError, SquareError, sameBody } from './model.js';
12
+ /** Persisted hook state keeps its wire coordinate; internal consumers keep the located seat. */
13
+ function claudeBinding({ location, seat }) {
14
+ return { location, participant: seat.participant, session: seat.body.session, generation: seat.generation };
15
+ }
12
16
  /** JSON stdin bridge. Identity is explicit per operation; process.env is never rewritten. */
13
17
  export async function runClaudeMod(inputText, env = process.env) {
14
18
  if (inputText.length > 512_000)
@@ -28,31 +32,30 @@ export async function runClaudeMod(inputText, env = process.env) {
28
32
  return '{}\n';
29
33
  }
30
34
  const scoped = operationEnv('claude', input.sessionId, env);
31
- const ledger = hostLedgerForEnv(env);
32
35
  const signal = AbortSignal.timeout(2500);
36
+ const body = { channel: 'claude-code', session: input.sessionId, ...(input.agentId === undefined ? {} : { agent: input.agentId }) };
33
37
  const supported = process.platform === 'darwin' && typeof input.endpoint === 'string' && input.endpoint.startsWith('/');
38
+ const sessionSeats = () => projectSessionSeats({ body, env }, signal);
34
39
  if (input.operation === 'end') {
35
40
  const retired = [];
36
41
  const captured = [...input.bindings ?? []];
37
42
  // Discover committed late joins at the pinned end boundary, never later owners.
38
43
  if (input.endedAt !== undefined)
39
- for (const row of await ledger.listPresence({ session: input.sessionId })) {
40
- if (row.channel === 'claude-code' && (row.updatedAt ?? Infinity) <= input.endedAt && !captured.some((binding) => binding.location === row.location && binding.participant === row.participant))
41
- captured.push(row);
44
+ for (const member of await sessionSeats()) {
45
+ if (member.joinedAt > input.endedAt || captured.some((item) => item.location === member.location
46
+ && item.participant === member.seat.participant && item.generation === member.seat.generation))
47
+ continue;
48
+ captured.push(claudeBinding(member));
42
49
  }
43
50
  for (const binding of captured) {
44
- if (binding.session !== input.sessionId || !Number.isSafeInteger(binding.epoch))
51
+ if (binding.session !== input.sessionId)
45
52
  continue;
46
- const square = await Square.at({ path: binding.location, hostLedger: ledger, env: scoped });
53
+ const square = await Square.at({ path: binding.location, env: scoped, body,
54
+ participant: binding.participant });
47
55
  try {
48
- const ended = await square.endOwnedSession(binding.participant, input.sessionId, binding.epoch);
49
- if (ended?.activity) {
50
- retired.push(binding);
51
- const row = { location: binding.location, participant: binding.participant, session: input.sessionId, activity: ended.activity.id, kind: 'lifecycle', bindingEpoch: binding.epoch, outcome: 'retired' };
52
- const claim = await ledger.claimEvidence({ ...row, leaseMs: 1000 });
53
- if (claim.status === 'acquired')
54
- await ledger.appendEvidence({ ...row, claimToken: claim.claimToken });
55
- }
56
+ const ended = await square.endOwnedSession(binding.participant, input.sessionId, binding.generation, body);
57
+ if (ended?.activity)
58
+ retired.push({ ...binding, generation: binding.generation });
56
59
  }
57
60
  finally {
58
61
  await square.close();
@@ -64,122 +67,133 @@ export async function runClaudeMod(inputText, env = process.env) {
64
67
  }
65
68
  const recovery = new Set();
66
69
  const cancelledBindings = [...input.cancelledBindings ?? []];
67
- if (input.operation === 'start') {
68
- // Host state survives reload, not conversation replacement. Resume authority is
69
- // the existing lifecycle ledger plus the exact ended activity, never peer text.
70
- if (input.resume)
71
- for (const row of await ledger.listEvidence({ kind: 'lifecycle', session: input.sessionId })) {
72
- if (signal.aborted || row.outcome !== 'retired' || !Number.isSafeInteger(row.bindingEpoch))
73
- continue;
74
- const probe = await openSquare(row.location, { hostLedger: ledger, env: scoped, signal });
75
- let eligible = false;
76
- try {
77
- const { value: last } = await probe.artifact.read((facts) => facts.activityHeads({
78
- actors: [row.participant], actorMatch: 'exact', kinds: ['join', 'done'],
79
- }, { edge: 'newest', limit: 1 }).items[0], signal);
80
- eligible = last?.kind === 'done' && `act/${last.index}` === row.activity;
81
- }
82
- finally {
83
- await closeOpenSquare(probe);
84
- }
85
- if (!eligible)
86
- continue;
87
- const claimed = await ledger.claimPresence({ location: row.location, participant: row.participant, session: input.sessionId, channel: 'claude-code', epoch: row.bindingEpoch + 1, updatedAt: Date.now() }, signal);
88
- if (claimed.status !== 'acquired')
70
+ if (input.operation === 'start' && input.resume) {
71
+ // Resume is proven by the artifact's latest participant lifecycle, while HOME
72
+ // contributes only candidate paths. A stale candidate never establishes a seat.
73
+ const candidates = await projectSessionCandidates({ body, env });
74
+ const seen = new Set();
75
+ for (const candidate of candidates) {
76
+ if (signal.aborted)
77
+ break;
78
+ const probe = createArtifactPort(candidate.location, undefined, signal);
79
+ let names;
80
+ try {
81
+ names = (await probe.read(facts => facts.completedSeatsForBody(body).map(seat => seat.participant), signal)).value;
82
+ }
83
+ catch (error) {
84
+ if (error instanceof InternalSquareError && error.code === 'not_found')
85
+ throw new SquareError('unavailable', `Square is unavailable at ${candidate.location}`);
86
+ throw error;
87
+ }
88
+ finally {
89
+ await probe.close();
90
+ }
91
+ for (const participant of names) {
92
+ const key = `${candidate.location}\0${participant}`;
93
+ if (seen.has(key))
89
94
  continue;
90
- const square = await Square.at({ path: row.location, hostLedger: ledger, env: scoped });
95
+ seen.add(key);
96
+ const square = await Square.at({ path: candidate.location, env: scoped, body, participant });
91
97
  try {
92
- await square.join(row.participant);
93
- }
94
- catch {
95
- await ledger.removePresenceIfUnchanged(claimed.record);
98
+ await square.join(participant);
96
99
  }
97
100
  finally {
98
101
  await square.close();
99
102
  }
100
103
  }
104
+ }
101
105
  }
102
106
  if (input.operation === 'start' || input.operation === 'reconcile' || input.operation === 'cancel' || input.cancelAt !== undefined) {
103
- const memberships = await ledger.listPresence({ session: input.sessionId });
104
- for (const binding of memberships) {
105
- if (binding.channel !== 'claude-code' || signal.aborted)
106
- continue;
107
- const expected = input.bindings?.find((row) => row.location === binding.location && row.participant === binding.participant && row.session === input.sessionId);
108
- const cancelled = input.cancelledBindings?.find((row) => row.location === binding.location && row.participant === binding.participant && row.session === input.sessionId)
109
- ?? (input.operation === 'cancel' ? expected : undefined);
110
- const lateCancelledJoin = input.operation === 'cancel' && input.cancelAt !== undefined && (binding.updatedAt ?? Infinity) <= input.cancelAt && expected === undefined;
111
- if (lateCancelledJoin)
112
- cancelledBindings.push(binding);
113
- if (input.operation !== 'start' && input.operation !== 'reconcile' && !lateCancelledJoin && (!expected || expected.epoch !== binding.epoch))
114
- continue;
115
- const claim = await claimSessionParticipant(binding.location, binding.participant, ledger, scoped, signal);
116
- if (!claim || claim.epoch !== (binding.epoch ?? 0))
107
+ for (const member of await sessionSeats()) {
108
+ if (signal.aborted)
109
+ break;
110
+ const { location, seat: capturedSeat, joinedAt } = member;
111
+ const matches = (item) => item !== undefined
112
+ && item.location === location && item.participant === capturedSeat.participant && item.session === capturedSeat.body.session
113
+ && (item.generation === undefined || item.generation === capturedSeat.generation);
114
+ const expected = input.bindings?.find(matches);
115
+ const cancelled = input.cancelledBindings?.find(matches) ?? (input.operation === 'cancel' ? expected : undefined);
116
+ const lateCancelledJoin = input.operation === 'cancel' && input.cancelAt !== undefined
117
+ && joinedAt <= input.cancelAt && expected === undefined;
118
+ if (lateCancelledJoin && !cancelledBindings.some(matches))
119
+ cancelledBindings.push(claudeBinding(member));
120
+ if (input.operation !== 'start' && input.operation !== 'reconcile' && !lateCancelledJoin && expected === undefined)
117
121
  continue;
118
- await ledger.withClaimLock(async () => {
119
- const current = (await ledger.listPresence({ location: binding.location, participant: binding.participant })).find((row) => row.session === input.sessionId && (row.epoch ?? 0) === claim.epoch);
120
- if (!current)
121
- return;
122
- const epoch = claim.epoch || 1;
123
- if (current.epoch !== epoch)
124
- await ledger.ensurePresence({ ...current, epoch });
125
- const square = await openSquare(binding.location, { hostLedger: ledger, env: scoped, signal });
126
- try {
127
- const { value: existing } = await square.artifact.read((facts) => facts.routes().find((route) => route.participant === binding.participant && route.sessionId === input.sessionId && route.kind === 'claude-native' && route.epoch === epoch), signal);
128
- if (!supported) {
129
- if (existing)
130
- await retireWakeRouteFromArtifact(square.artifact, existing, { expectedEpoch: epoch });
131
- return;
132
- }
133
- const cutoff = cancelled?.epoch === epoch || (lateCancelledJoin && current.epoch === binding.epoch && (current.updatedAt ?? Infinity) <= input.cancelAt) ? input.cancelAt : undefined;
134
- if (cutoff !== undefined) {
135
- const { value: through } = await square.artifact.read((facts) => {
136
- let beforeIndex;
137
- for (;;) {
138
- const page = facts.activityHeads({ beforeIndex }, { edge: 'newest', limit: 100 });
139
- const last = [...page.items].reverse().find((activity) => activity.at <= cutoff);
140
- if (last !== undefined)
141
- return last.index;
142
- if (!page.hasMore)
143
- return -1;
144
- beforeIndex = page.items[0].index;
145
- }
146
- }, signal);
147
- await ledger.suppressPresence({ ...current, epoch }, through);
148
- }
149
- if (!existing || existing.address.endpoint !== input.endpoint)
150
- recovery.add(binding.location);
151
- await publishWakeRoute(square.artifact, { location: binding.location, participant: binding.participant, sessionId: input.sessionId, channel: 'claude-code', kind: 'claude-native', epoch, address: { sessionId: input.sessionId, endpoint: input.endpoint, version: input.version, platform: 'darwin', loaded: 'true' } }, { requireCurrentSession: true });
122
+ const artifact = createArtifactPort(location, undefined, signal);
123
+ try {
124
+ const { value: currentSeat } = await artifact.read((facts) => facts.seat(capturedSeat.participant), signal);
125
+ if (currentSeat === undefined || currentSeat.generation !== capturedSeat.generation
126
+ || !sameBody(currentSeat.body, capturedSeat.body))
127
+ continue;
128
+ const seat = asSeatRef(currentSeat);
129
+ const existing = currentSeat.wakeAddress?.kind === 'claude-native' ? currentSeat.wakeAddress : undefined;
130
+ const cutoff = input.cancelAt !== undefined && (cancelled !== undefined || lateCancelledJoin) ? input.cancelAt : undefined;
131
+ if (cutoff !== undefined) {
132
+ const { value: through } = await artifact.read((facts) => {
133
+ let beforeIndex;
134
+ for (;;) {
135
+ const page = facts.activityHeads({ beforeIndex }, { edge: 'newest', limit: 100 });
136
+ const last = [...page.items].reverse().find((activity) => activity.at <= cutoff);
137
+ if (last !== undefined)
138
+ return last.index;
139
+ if (!page.hasMore)
140
+ return -1;
141
+ beforeIndex = page.items[0].index;
142
+ }
143
+ }, signal);
144
+ if (through >= 0)
145
+ await artifact.cancelDeliveries(seat, through, signal);
152
146
  }
153
- finally {
154
- await closeOpenSquare(square);
147
+ if (!supported) {
148
+ if (existing !== undefined)
149
+ await artifact.transactAs({ kind: 'participant', seat }, (facts) => facts.updateWakeAddress(seat, undefined), signal);
150
+ continue;
155
151
  }
156
- }, signal);
152
+ if (existing === undefined || existing.address.endpoint !== input.endpoint)
153
+ recovery.add(location);
154
+ await artifact.transactAs({ kind: 'participant', seat }, (facts) => facts.updateWakeAddress(seat, {
155
+ kind: 'claude-native', address: { sessionId: input.sessionId, endpoint: input.endpoint, version: input.version, platform: 'darwin', loaded: 'true' },
156
+ }), signal);
157
+ }
158
+ catch (error) {
159
+ if (error instanceof InternalSquareError && error.code === 'not_found')
160
+ throw new SquareError('unavailable', `Square is unavailable at ${location}`);
161
+ throw error;
162
+ }
163
+ finally {
164
+ await artifact.close();
165
+ }
157
166
  }
158
167
  }
159
168
  if (input.operation === 'start' || input.operation === 'reconcile' || input.operation === 'cancel') {
160
- const bindings = (await ledger.listPresence({ session: input.sessionId })).filter((row) => row.channel === 'claude-code').map(({ location, participant, session, epoch }) => ({ location, participant, session, epoch }));
169
+ const members = await sessionSeats();
161
170
  // One startup recovery pass for known unsent work. Unknown/admitted sends never replay.
162
171
  if (supported && input.operation !== 'cancel')
163
- for (const binding of bindings) {
172
+ for (const { location } of members) {
164
173
  if (signal.aborted)
165
174
  break;
166
- if (input.operation !== 'start' && !recovery.has(binding.location))
175
+ if (input.operation !== 'start' && !recovery.has(location))
167
176
  continue;
168
- const square = await openSquare(binding.location, { hostLedger: ledger, env, signal });
177
+ const artifact = createArtifactPort(location, undefined, signal);
169
178
  let indices;
170
179
  try {
171
- indices = await sweepPending({ artifact: square.artifact, hostLedger: ledger, location: binding.location, now: Date.now(), graceMs: 0, limit: 8 });
180
+ indices = await sweepPending({ artifact, location, now: Date.now(), graceMs: 0, limit: 8 });
181
+ }
182
+ catch (error) {
183
+ if (error instanceof InternalSquareError && error.code === 'not_found')
184
+ throw new SquareError('unavailable', `Square is unavailable at ${location}`);
185
+ throw error;
172
186
  }
173
187
  finally {
174
- await closeOpenSquare(square);
188
+ await artifact.close();
175
189
  }
176
190
  for (const index of indices) {
177
191
  if (signal.aborted)
178
192
  break;
179
- await processActNotificationsOnce(binding.location, index, { env: { ...env, SQUARE_NOTIFY_DELIVERY_WAIT_MS: '200' } });
193
+ await processActNotificationsOnce(location, index, { env: { ...env, SQUARE_NOTIFY_DELIVERY_WAIT_MS: '200' } });
180
194
  }
181
195
  }
182
- return `${JSON.stringify({ available: supported, diagnostic: supported ? undefined : 'Native inbox unavailable: requires macOS Claude with an interactive loaded mod and its real endpoint.', bindings, cancelledBindings })}\n`;
196
+ return `${JSON.stringify({ available: supported, diagnostic: supported ? undefined : 'Native inbox unavailable: requires macOS Claude with an interactive loaded mod and its real endpoint.', bindings: members.map(claudeBinding), cancelledBindings })}\n`;
183
197
  }
184
198
  if (!['guard', 'admitted', 'stored'].includes(input.operation) || typeof input.text !== 'string')
185
199
  throw new Error('Unknown Claude bridge operation.');
@@ -1,6 +1,6 @@
1
1
  import { type HardCap } from '../model.js';
2
2
  import { Square } from '../square-api.js';
3
- import type { OperationControl } from '../square-facade.js';
3
+ import type { Body, OperationControl } from '../square-facade.js';
4
4
  export interface CommandContext {
5
5
  cwd: string;
6
6
  env: NodeJS.ProcessEnv;
@@ -36,6 +36,8 @@ export declare function parseGlobalArgs(rawArgs: string[]): Promise<ParsedGlobal
36
36
  export declare function locationIsRequired(command: string): boolean;
37
37
  export declare function defaultContext(command: string, squarePath?: string, name?: string): CommandContext;
38
38
  export declare function squareInput(context: CommandContext): {
39
+ wakeAddress?: import("../model.js").WakeAddress | undefined;
40
+ body?: Body | undefined;
39
41
  cwd: string;
40
42
  env: NodeJS.ProcessEnv;
41
43
  path: string | undefined;