@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,60 +1,63 @@
1
- import { nameKey } from '../../model.js';
2
- import { nativePendingPreview, observeSessionPending, sessionInbox } from '../../inbox.js';
3
- import { hostLedgerForEnv } from '../../registry.js';
4
- import { openSquare } from '../../open-square.js';
5
- import { closeOpenSquare } from '../../open-square.js';
6
- import { presentationSuppressesWake, projectPresentationEvidence } from '../../square-projections.js';
7
- import { presentPending } from '../../presentation-operations.js';
1
+ import { createHash } from 'node:crypto';
2
+ import { createArtifactPort } from '../../artifact.js';
8
3
  import { deliverPending } from '../../delivery-operations.js';
9
- import { createWakeTransport } from '../../notifications.js';
10
- import { publishWakeRoute } from '../../routes.js';
4
+ import { nativePendingPreview, observeSessionPending, projectSessionSeats } from '../../inbox.js';
5
+ import { automaticWakeEligible, presentationBlocksWake } from '../../delivery.js';
6
+ import { sameBody } from '../../model.js';
11
7
  import { formatActivityId, parseActivityId } from '../../square-core.js';
8
+ import { indexedSquareEntries } from '../../square-index.js';
9
+ import { asSeatRef } from '../../seats.js';
10
+ import { presentPending } from '../../presentation-operations.js';
12
11
  import { createNativeInputId, sendNativeText } from '../../packages/agent-delivery/src/opencode-native.js';
13
12
  const routeKind = 'opencode-server';
14
- const key = (location, participant, index) => `${location}\0${nameKey(participant)}\0${index}`;
15
- /** The one native pending preview: the audience/catch/epoch fence before send and inside presentation. */
16
- function preview(facts, owner, index) {
17
- return nativePendingPreview(facts, { location: owner.location, routeKind, sessionId: owner.session, participant: owner.participant, actIndex: index, epoch: owner.epoch, address: { sessionId: owner.session }, cancelledThrough: owner.cancelledThrough, now: Date.now() });
13
+ const bodyFor = (sessionId) => ({ channel: 'opencode', session: sessionId });
14
+ const payloadHash = (payload) => createHash('sha256').update(payload).digest('hex');
15
+ function preview(facts, location, seat, index) {
16
+ return nativePendingPreview(facts, {
17
+ location, routeKind, sessionId: seat.body.session, participant: seat.participant,
18
+ actIndex: index, address: { sessionId: seat.body.session }, now: Date.now(),
19
+ });
20
+ }
21
+ export async function indexedOpenCodeSessions(env) {
22
+ return [...new Set((await indexedSquareEntries({ channel: 'opencode' }, env)).map((entry) => entry.body.session))];
23
+ }
24
+ export async function discoverOpenCodeSeats(sessionId, env, signal) {
25
+ return projectSessionSeats({ body: bodyFor(sessionId), env }, signal);
26
+ }
27
+ function sameSeat(left, right) {
28
+ return left !== undefined && left.generation === right.generation && sameBody(left.body, right.body);
18
29
  }
19
- function createOpenCodeWakeTransport(target, env, lifetime) {
20
- const ledger = hostLedgerForEnv(env);
30
+ function createOpenCodeWakeTransport(target, lifetime) {
21
31
  return {
22
- probe: async (route) => !lifetime.aborted && route.kind === routeKind && route.sessionId === target.sessionId && route.address.sessionId === target.sessionId,
32
+ probe: async (route) => !lifetime.aborted && route.kind === routeKind
33
+ && route.sessionId === target.sessionId && route.address.sessionId === target.sessionId,
23
34
  async attempt(request, timeoutMs, beforeSend) {
35
+ const index = parseActivityId(request.activity);
36
+ const artifact = request.artifact;
37
+ if (artifact === undefined || request.claimToken === undefined || index === undefined
38
+ || !sameBody(request.seat.body, bodyFor(target.sessionId))
39
+ || request.route.kind !== routeKind || request.route.address.sessionId !== target.sessionId) {
40
+ return { outcome: 'not-capable', diagnostic: 'OpenCode attention has no matching artifact claim or session.' };
41
+ }
24
42
  const signal = AbortSignal.any([lifetime, AbortSignal.timeout(timeoutMs)]);
25
- const deadline = Date.now() + timeoutMs;
26
- // Start the native call under the ownership fence, but await it OUTSIDE: prompt may
27
- // enter our context hook synchronously and that hook needs the same ownership lock.
28
- const started = await ledger.withClaimLock(async () => {
29
- if (signal.aborted || request.route.sessionId !== target.sessionId || !request.claimToken
30
- || !await (beforeSend?.() ?? Promise.resolve(true)))
31
- return undefined;
32
- const owner = (await ledger.listPresence({ location: request.location, participant: request.participant, session: target.sessionId }))
33
- .find((binding) => binding.channel === 'opencode' && binding.epoch === request.route.epoch);
34
- const index = parseActivityId(request.activity);
35
- if (!owner || index === undefined)
36
- return undefined;
37
- const square = await openSquare(owner.location, { hostLedger: ledger, env, signal });
38
- try {
39
- const { value: prepared } = await square.artifact.read((facts) => preview(facts, owner, index), signal);
40
- if (!prepared)
41
- return undefined;
42
- const row = { location: owner.location, participant: owner.participant, session: target.sessionId,
43
- activity: request.activity, kind: 'wake', outcome: 'dispatching', routeKind, attemptN: request.attemptN, claimToken: request.claimToken,
44
- nativeDelivery: { harness: 'opencode', epoch: owner.epoch ?? 0, inputId: createNativeInputId(), payload: prepared.payload } };
45
- if (!await ledger.prepareNativeWake(row) || !await (beforeSend?.() ?? Promise.resolve(true)) || signal.aborted
46
- || (await square.artifact.read((facts) => preview(facts, owner, index)?.payload, signal)).value !== prepared.payload)
47
- return undefined;
48
- return { sending: sendNativeText(target, prepared.payload, { inputId: row.nativeDelivery.inputId, delivery: 'steer',
49
- timeoutMs: Math.max(1, deadline - Date.now()), signal }) };
50
- }
51
- finally {
52
- await closeOpenSquare(square);
53
- }
54
- }, signal);
55
- if (!started)
56
- return { outcome: 'not-capable', diagnostic: 'OpenCode attention, binding or local delivery generation changed before send.' };
57
- const result = await started.sending;
43
+ const started = Date.now();
44
+ if (signal.aborted || !await beforeSend())
45
+ return { outcome: 'not-capable', diagnostic: 'OpenCode attention changed before send.' };
46
+ const { value: prepared } = await artifact.read((facts) => preview(facts, request.location, request.seat, index), signal);
47
+ if (prepared === undefined)
48
+ return { outcome: 'not-capable', diagnostic: 'OpenCode attention is no longer current.' };
49
+ const inputId = createNativeInputId();
50
+ const correlation = { harness: 'opencode', token: inputId, payloadHash: payloadHash(prepared.payload) };
51
+ if (!await artifact.prepareDelivery({ seat: request.seat, claimToken: request.claimToken, correlation }, signal))
52
+ return { outcome: 'not-capable', diagnostic: 'OpenCode delivery claim is no longer current.' };
53
+ const fresh = await artifact.read((facts) => preview(facts, request.location, request.seat, index), signal);
54
+ if (fresh.value === undefined || payloadHash(fresh.value.payload) !== correlation.payloadHash)
55
+ return { outcome: 'not-capable', diagnostic: 'OpenCode attention changed before native send.' };
56
+ if (signal.aborted || !await beforeSend())
57
+ return { outcome: 'not-capable', diagnostic: 'OpenCode attention was consumed before send.' };
58
+ const result = await sendNativeText(target, prepared.payload, {
59
+ inputId, delivery: 'steer', timeoutMs: Math.max(1, timeoutMs - (Date.now() - started)), signal,
60
+ });
58
61
  if (result.state === 'accepted')
59
62
  return { outcome: 'accepted' };
60
63
  if (result.state === 'unavailable')
@@ -63,109 +66,161 @@ function createOpenCodeWakeTransport(target, env, lifetime) {
63
66
  },
64
67
  };
65
68
  }
66
- /** Only the matching prepared primary context authorizes presentation, never admission. */
69
+ function textMessage(message) {
70
+ if (message.role !== 'user' || message.id === undefined || !message.content.every((part) => part.type === 'text' && typeof part.text === 'string'))
71
+ return undefined;
72
+ return message.content.map((part) => part.text).join('');
73
+ }
74
+ /** The exact prepared native message can authorize artifact-backed presentation once. */
67
75
  export async function observeOpenCodeContext(sessionId, messages, env, signal) {
68
- const ledger = hostLedgerForEnv(env);
69
- await ledger.withClaimLock(async () => {
76
+ const observed = new Map(messages.flatMap((message) => {
77
+ const text = textMessage(message);
78
+ return text === undefined ? [] : [[message.id, payloadHash(text)]];
79
+ }));
80
+ if (observed.size === 0 || signal.aborted)
81
+ return;
82
+ for (const { location, seat } of await discoverOpenCodeSeats(sessionId, env, signal)) {
70
83
  if (signal.aborted)
71
84
  return;
72
- const rows = (await ledger.listEvidence({ kind: 'wake', session: sessionId })).filter((row) => row.nativeDelivery?.harness === 'opencode' && !!row.claimToken && ['dispatching', 'unknown', 'accepted'].includes(row.outcome));
73
- for (const row of rows) {
74
- if (signal.aborted || !messages.some((message) => message.id === row.nativeDelivery.inputId && message.role === 'user'
75
- && message.content.every((part) => part.type === 'text' && typeof part.text === 'string')
76
- && message.content.map((part) => part.text).join('') === row.nativeDelivery.payload))
77
- continue;
78
- const owner = (await ledger.listPresence({ location: row.location, participant: row.participant, session: sessionId }))
79
- .find((binding) => binding.channel === 'opencode' && binding.epoch === row.nativeDelivery.epoch);
80
- const index = parseActivityId(row.activity);
81
- if (!owner || index === undefined)
82
- continue;
83
- const square = await openSquare(row.location, { hostLedger: ledger, env, signal });
84
- try {
85
- const { value: prepared } = await square.artifact.read((facts) => preview(facts, owner, index), signal);
86
- if (!prepared || prepared.payload !== row.nativeDelivery.payload)
87
- continue;
88
- const prior = await projectPresentationEvidence({ hostLedger: ledger, location: row.location, participant: row.participant, sessionId, activity: row.activity });
89
- if (presentationSuppressesWake(prior))
90
- continue;
91
- await presentPending({ artifact: square.artifact, hostLedger: ledger, location: row.location, participant: row.participant, session: sessionId,
92
- activity: row.activity, sink: { present() { } }, markSeen: !prepared.clipped, signal,
93
- current: (facts) => !signal.aborted && preview(facts, owner, index)?.payload === row.nativeDelivery.payload });
94
- }
95
- finally {
96
- await closeOpenSquare(square);
85
+ const artifact = createArtifactPort(location, undefined, signal);
86
+ try {
87
+ const { value: matches } = await artifact.read((facts) => {
88
+ if (!sameSeat(facts.seat(seat.participant), seat))
89
+ return [];
90
+ return facts.deliveryEvidence(seat.participant, seat.generation).flatMap((evidence) => {
91
+ const token = evidence.correlation?.harness === 'opencode' ? evidence.correlation.token : undefined;
92
+ const hash = token === undefined ? undefined : observed.get(token);
93
+ const index = parseActivityId(evidence.activity);
94
+ if (token === undefined || hash === undefined || evidence.claimToken === undefined || index === undefined
95
+ || !['dispatching', 'unknown', 'accepted'].includes(evidence.outcome)
96
+ || evidence.correlation?.payloadHash !== hash)
97
+ return [];
98
+ const prepared = preview(facts, location, seat, index);
99
+ if (prepared === undefined || payloadHash(prepared.payload) !== hash
100
+ || presentationBlocksWake(facts.deliveryEvidence(seat.participant, seat.generation, evidence.activity), Date.now()))
101
+ return [];
102
+ return [{ evidence, index, hash, clipped: prepared.clipped }];
103
+ });
104
+ }, signal);
105
+ for (const { evidence, index, hash, clipped } of matches) {
106
+ if (signal.aborted || evidence.claimToken === undefined)
107
+ return;
108
+ await presentPending({ artifact, location, participant: seat.participant,
109
+ activity: formatActivityId(index), seat: asSeatRef(seat), sink: { present() { } }, markSeen: !clipped, signal,
110
+ current: (facts) => {
111
+ const current = facts.seat(seat.participant);
112
+ const prepared = current === undefined || !sameSeat(current, seat) ? undefined : preview(facts, location, seat, index);
113
+ const correlation = facts.deliveryByToken(evidence.claimToken);
114
+ return prepared !== undefined && payloadHash(prepared.payload) === hash
115
+ && correlation?.correlation?.harness === 'opencode'
116
+ && correlation.correlation.token !== undefined
117
+ && correlation.correlation.payloadHash === hash;
118
+ },
119
+ });
97
120
  }
98
121
  }
99
- }, signal);
122
+ finally {
123
+ await artifact.close();
124
+ }
125
+ }
100
126
  }
101
- /** One existing observer and the existing dispatch owner; no second queue or evidence store. */
127
+ /** The native receiver owns no claims outside the artifact; HOME is used only for discovery. */
102
128
  export async function receiveOpenCodePending(target, env, signal) {
103
- const ledger = hostLedgerForEnv(env);
104
- const observer = await observeSessionPending(target.sessionId, env);
105
- const excluded = new Set();
106
- const refreshExclusions = async () => {
107
- excluded.clear();
108
- const owners = await ledger.listPresence({ session: target.sessionId });
109
- for (const row of await ledger.listEvidence({ kind: 'wake', session: target.sessionId })) {
110
- const index = parseActivityId(row.activity);
111
- const current = owners.some((owner) => owner.location === row.location && nameKey(owner.participant) === nameKey(row.participant) && owner.epoch === row.nativeDelivery?.epoch);
112
- if (index !== undefined && (['accepted', 'unknown'].includes(row.outcome) || row.outcome === 'dispatching' && current && row.nativeDelivery?.harness === 'opencode'))
113
- excluded.add(key(row.location, row.participant, index));
114
- }
115
- for (const membership of await sessionInbox(target.sessionId, env, signal)) {
116
- const owner = owners.find((binding) => binding.location === membership.squarePath && nameKey(binding.participant) === nameKey(membership.name));
117
- for (const note of membership.notifications)
118
- if (note.actIndex <= (owner?.cancelledThrough ?? -1))
119
- excluded.add(key(membership.squarePath, membership.name, note.actIndex));
120
- }
121
- };
122
- const transport = createWakeTransport([], ledger, Date.now, { [routeKind]: createOpenCodeWakeTransport(target, env, signal) });
129
+ const observer = await observeSessionPending(bodyFor(target.sessionId), env);
130
+ const transport = createOpenCodeWakeTransport(target, signal);
131
+ let waiting;
123
132
  try {
124
- await refreshExclusions();
125
- let pending = await observer.wait(Infinity, { signal, excludeKeys: excluded });
126
- while (!signal.aborted && pending.length > 0) {
127
- let armed;
128
- const ready = new Promise((resolve) => { armed = resolve; });
129
- // Capture the next state edge BEFORE dispatch. Evidence failures alone do not retry.
130
- const next = observer.wait(Infinity, { signal, excludeKeys: excluded, skipImmediate: true, onChangeArmed: armed })
131
- .catch(() => { armed(false); return []; });
132
- if (!await ready || signal.aborted)
133
+ while (!signal.aborted) {
134
+ const armed = new Promise((resolve) => {
135
+ waiting = observer.wait(Infinity, { signal, skipImmediate: true, onChangeArmed: resolve });
136
+ void waiting.catch(() => resolve(false));
137
+ });
138
+ if (!await armed) {
139
+ await waiting;
133
140
  break;
134
- for (const membership of pending) {
135
- const square = await openSquare(membership.squarePath, { hostLedger: ledger, env, signal });
141
+ }
142
+ for (const membership of await observer.read({ signal })) {
143
+ const { location, seat } = membership;
144
+ if (signal.aborted)
145
+ return;
146
+ const artifact = createArtifactPort(location, undefined, signal);
136
147
  try {
137
- for (const note of membership.notifications) {
138
- if (signal.aborted || excluded.has(key(membership.squarePath, membership.name, note.actIndex)))
139
- continue;
140
- await deliverPending({ artifact: square.artifact, hostLedger: ledger, transport, location: square.location, activity: formatActivityId(note.actIndex), now: Date.now() });
148
+ const { value: notifications } = await artifact.read((facts) => {
149
+ if (!sameSeat(facts.seat(seat.participant), seat))
150
+ return [];
151
+ return membership.notifications.filter((note) => automaticWakeEligible(facts.deliveryEvidence(seat.participant, seat.generation, formatActivityId(note.actIndex)), Date.now()));
152
+ }, signal);
153
+ for (const note of notifications) {
154
+ if (signal.aborted)
155
+ return;
156
+ await deliverPending({ artifact, transport, location, activity: formatActivityId(note.actIndex), now: Date.now() });
141
157
  }
142
158
  }
159
+ catch (error) {
160
+ if (signal.aborted)
161
+ throw signal.reason ?? error;
162
+ // A candidate that became unreadable cannot authorize sending; the shared observer
163
+ // keeps its subscription and will reconsider the artifact after repair.
164
+ }
143
165
  finally {
144
- await closeOpenSquare(square);
166
+ await artifact.close();
145
167
  }
146
168
  }
147
- await refreshExclusions();
148
- pending = await next;
169
+ await waiting;
170
+ waiting = undefined;
149
171
  }
150
172
  }
173
+ catch (error) {
174
+ if (!signal.aborted)
175
+ throw error;
176
+ }
151
177
  finally {
152
178
  observer.close();
179
+ await waiting?.catch(() => { });
153
180
  }
154
181
  }
155
- export async function publishOpenCodeRoutes(sessionId, env, signal) {
156
- const ledger = hostLedgerForEnv(env);
157
- return ledger.withClaimLock(async () => {
158
- const owners = (await ledger.listPresence({ session: sessionId })).filter((binding) => binding.channel === 'opencode');
159
- for (const owner of owners) {
160
- const square = await openSquare(owner.location, { hostLedger: ledger, env, signal });
161
- try {
162
- await publishWakeRoute(square.artifact, { location: owner.location, participant: owner.participant, sessionId, channel: 'opencode',
163
- kind: routeKind, address: { sessionId }, epoch: owner.epoch }, { requireCurrentSession: true });
164
- }
165
- finally {
166
- await closeOpenSquare(square);
167
- }
182
+ /** Publish the native address into current artifact seats; the legacy route projection is not read. */
183
+ export async function publishOpenCodeAddress(sessionId, env, signal) {
184
+ const seats = await discoverOpenCodeSeats(sessionId, env, signal);
185
+ for (const { location, seat } of seats) {
186
+ if (signal.aborted)
187
+ return false;
188
+ const artifact = createArtifactPort(location, undefined, signal);
189
+ try {
190
+ await artifact.transactAs({ kind: 'participant', seat: asSeatRef(seat) }, (facts) => facts.updateWakeAddress(asSeatRef(seat), { kind: routeKind, address: { sessionId } }));
168
191
  }
169
- return owners.length > 0;
170
- }, signal);
192
+ finally {
193
+ await artifact.close();
194
+ }
195
+ }
196
+ return seats.length > 0;
197
+ }
198
+ export async function cancelOpenCodeDeliveries(sessionId, env, signal) {
199
+ for (const { location, seat } of await discoverOpenCodeSeats(sessionId, env, signal)) {
200
+ if (signal.aborted)
201
+ return;
202
+ const artifact = createArtifactPort(location, undefined, signal);
203
+ try {
204
+ const { value: through } = await artifact.read((facts) => facts.header().lastActIndex, signal);
205
+ if (through >= 0)
206
+ await artifact.cancelDeliveries(asSeatRef(seat), through, signal);
207
+ }
208
+ finally {
209
+ await artifact.close();
210
+ }
211
+ }
212
+ }
213
+ export async function endOpenCodeSession(sessionId, env, signal) {
214
+ for (const { location, seat } of await discoverOpenCodeSeats(sessionId, env, signal)) {
215
+ if (signal.aborted)
216
+ return;
217
+ const { Square } = await import('../../square-api.js');
218
+ const square = await Square.at({ path: location, env, body: seat.body, participant: seat.participant });
219
+ try {
220
+ await square.endOwnedSession(seat.participant, sessionId, seat.generation, seat.body);
221
+ }
222
+ finally {
223
+ await square.close();
224
+ }
225
+ }
171
226
  }
@@ -1,11 +1,7 @@
1
- import { operationEnv } from '../../participant-identity.js';
2
- import { hostLedgerForEnv } from '../../registry.js';
1
+ import { observeSquareIndex } from '../../square-index.js';
3
2
  import { canonicalPath } from '../../canonical-path.js';
4
- import { openSquare } from '../../open-square.js';
5
- import { closeOpenSquare } from '../../open-square.js';
6
- import { Square } from '../../square-api.js';
7
3
  import { connectNative } from '../../packages/agent-delivery/src/opencode-native.js';
8
- import { observeOpenCodeContext, publishOpenCodeRoutes, receiveOpenCodePending } from './delivery.js';
4
+ import { cancelOpenCodeDeliveries, endOpenCodeSession, indexedOpenCodeSessions, observeOpenCodeContext, publishOpenCodeAddress, receiveOpenCodePending } from './delivery.js';
9
5
  /** Actual OpenCode 2.0.20 server plugin. Harness identity belongs to ctx, never process.env. */
10
6
  export default {
11
7
  id: 'square',
@@ -13,7 +9,7 @@ export default {
13
9
  if (ctx.app.version !== '2.0.20')
14
10
  return async () => { };
15
11
  const env = { ...process.env };
16
- const ledger = hostLedgerForEnv(env);
12
+ const index = await observeSquareIndex(env);
17
13
  const lifetime = new AbortController();
18
14
  const receivers = new Map(); // Operational handles only; presence owns membership.
19
15
  const directory = await canonicalPath(ctx.location.directory);
@@ -43,7 +39,7 @@ export default {
43
39
  return target;
44
40
  };
45
41
  const reconcile = async (activeSession) => {
46
- const candidates = new Set((await ledger.listPresence({})).filter((binding) => binding.channel === 'opencode').map((binding) => binding.session));
42
+ const candidates = new Set(await indexedOpenCodeSessions(env));
47
43
  if (activeSession)
48
44
  candidates.add(activeSession);
49
45
  for (const sessionId of candidates) {
@@ -54,8 +50,7 @@ export default {
54
50
  stop(sessionId);
55
51
  continue;
56
52
  }
57
- const scopedEnv = operationEnv('opencode', sessionId, env);
58
- if (!await publishOpenCodeRoutes(sessionId, scopedEnv, lifetime.signal)) {
53
+ if (!await publishOpenCodeAddress(sessionId, env, lifetime.signal)) {
59
54
  stop(sessionId);
60
55
  continue;
61
56
  }
@@ -63,7 +58,7 @@ export default {
63
58
  continue;
64
59
  const abort = new AbortController();
65
60
  const signal = AbortSignal.any([lifetime.signal, abort.signal]);
66
- const receiver = { abort, settled: false, running: receiveOpenCodePending(target, scopedEnv, signal).catch(() => { }).finally(() => { receiver.settled = true; }) };
61
+ const receiver = { abort, settled: false, running: receiveOpenCodePending(target, env, signal).catch(() => { }).finally(() => { receiver.settled = true; }) };
67
62
  receivers.set(sessionId, receiver);
68
63
  }
69
64
  for (const sessionId of receivers.keys())
@@ -75,31 +70,12 @@ export default {
75
70
  return;
76
71
  // Stop synchronously; persist the cutoff before allowing reconciliation to start fresh work.
77
72
  stop(sessionId);
78
- await ledger.withClaimLock(async () => {
79
- for (const owner of (await ledger.listPresence({ session: sessionId })).filter((binding) => binding.channel === 'opencode')) {
80
- const square = await openSquare(owner.location, { hostLedger: ledger, env, signal: lifetime.signal });
81
- try {
82
- await ledger.suppressPresence(owner, (await square.artifact.read((facts) => facts.header().nextActIndex - 1, lifetime.signal)).value);
83
- }
84
- finally {
85
- await closeOpenSquare(square);
86
- }
87
- }
88
- }, lifetime.signal);
73
+ await cancelOpenCodeDeliveries(sessionId, env, lifetime.signal);
89
74
  await reconcile();
90
75
  };
91
76
  const end = async (sessionId) => {
92
77
  stop(sessionId);
93
- const memberships = (await ledger.listPresence({ session: sessionId })).filter((binding) => binding.channel === 'opencode');
94
- for (const owner of memberships) {
95
- const square = await Square.at({ path: owner.location, hostLedger: ledger, env: operationEnv('opencode', sessionId, env) });
96
- try {
97
- await square.endOwnedSession(owner.participant, sessionId, owner.epoch);
98
- }
99
- finally {
100
- await square.close();
101
- }
102
- }
78
+ await endOpenCodeSession(sessionId, env, lifetime.signal);
103
79
  };
104
80
  await ctx.session.hook('prompt', (event) => coordinate(() => reconcile(event.sessionID)));
105
81
  await ctx.session.hook('context', async (event) => {
@@ -107,15 +83,13 @@ export default {
107
83
  const receiver = receivers.get(event.sessionID);
108
84
  if (!receiver || lifetime.signal.aborted)
109
85
  return;
110
- await observeOpenCodeContext(event.sessionID, event.messages, operationEnv('opencode', event.sessionID, env), AbortSignal.any([lifetime.signal, receiver.abort.signal])).catch(() => { });
86
+ await observeOpenCodeContext(event.sessionID, event.messages, env, AbortSignal.any([lifetime.signal, receiver.abort.signal])).catch(() => { });
111
87
  });
112
- const changes = await ledger.observeChanges();
113
- changes.evidence.close(); // Manual joins activate an idle receiver; evidence already has a session observer.
114
88
  const presenceLoop = (async () => {
115
89
  while (!lifetime.signal.aborted) {
116
- const baseline = await changes.presence.read(lifetime.signal);
90
+ const baseline = await index.read(lifetime.signal);
117
91
  await coordinate(() => reconcile());
118
- await changes.presence.changed(baseline, Infinity, lifetime.signal);
92
+ await index.changed(baseline, Infinity, lifetime.signal);
119
93
  }
120
94
  })().catch(() => { });
121
95
  const eventLoop = (async () => {
@@ -136,7 +110,7 @@ export default {
136
110
  // Do not await any native prompt in setup: plugin activation itself is lazy.
137
111
  return async () => {
138
112
  lifetime.abort();
139
- changes.presence.close();
113
+ index.close();
140
114
  const tasks = [presenceLoop, eventLoop, coordinating, ...[...receivers.keys()].map(stop)];
141
115
  let timer;
142
116
  try {