@astrosheep/square 0.3.86 → 0.3.87
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/artifact.d.ts +6 -1
- package/dist/artifact.js +79 -10
- package/dist/boundary-presentation.d.ts +5 -4
- package/dist/boundary-presentation.js +21 -25
- package/dist/catch-decisions.js +2 -1
- package/dist/claude-delivery-status.d.ts +8 -2
- package/dist/claude-delivery-status.js +26 -10
- package/dist/claude-delivery.d.ts +8 -4
- package/dist/claude-delivery.js +114 -104
- package/dist/claude-mcp-context.js +2 -2
- package/dist/claude-mod.js +119 -105
- package/dist/cli/context.d.ts +3 -1
- package/dist/cli/context.js +25 -1
- package/dist/cli/express-presentation.d.ts +3 -1
- package/dist/cli/express-presentation.js +14 -10
- package/dist/cli/mcp-server-command.js +2 -2
- package/dist/cli/observation-commands.js +1 -0
- package/dist/cli/registry.js +0 -2
- package/dist/cli/square-commands.d.ts +4 -2
- package/dist/cli/square-commands.js +21 -6
- package/dist/codex-hook.d.ts +3 -3
- package/dist/codex-hook.js +4 -4
- package/dist/codex-queue.d.ts +5 -6
- package/dist/codex-queue.js +9 -5
- package/dist/decisions.d.ts +3 -2
- package/dist/decisions.js +18 -6
- package/dist/delivery-health.d.ts +32 -2
- package/dist/delivery-health.js +108 -45
- package/dist/delivery-operations.d.ts +1 -2
- package/dist/delivery-operations.js +155 -196
- package/dist/delivery.d.ts +14 -23
- package/dist/delivery.js +48 -18
- package/dist/file-lock.d.ts +8 -1
- package/dist/file-lock.js +2 -0
- package/dist/help.js +8 -11
- package/dist/inbox.d.ts +35 -9
- package/dist/inbox.js +228 -144
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1 -3
- package/dist/list.js +9 -4
- package/dist/mcp.js +12 -1
- package/dist/model.d.ts +39 -5
- package/dist/model.js +84 -0
- package/dist/notifications.d.ts +8 -6
- package/dist/notifications.js +56 -143
- package/dist/operation-presentation.js +3 -4
- package/dist/participant-identity.d.ts +0 -3
- package/dist/participant-identity.js +0 -31
- package/dist/paseo-delivery.d.ts +5 -6
- package/dist/paseo-delivery.js +13 -8
- package/dist/paseo.d.ts +1 -1
- package/dist/paseo.js +1 -1
- package/dist/plugins/opencode/delivery.d.ts +8 -3
- package/dist/plugins/opencode/delivery.js +187 -132
- package/dist/plugins/opencode/index.js +12 -38
- package/dist/ports.d.ts +112 -40
- package/dist/presentation-operations.d.ts +1 -1
- package/dist/presentation-operations.js +51 -35
- package/dist/presentation.js +8 -3
- package/dist/seats.d.ts +7 -0
- package/dist/seats.js +27 -0
- package/dist/session-lifecycle.d.ts +2 -2
- package/dist/session-lifecycle.js +10 -12
- package/dist/sqlite-square.d.ts +20 -9
- package/dist/sqlite-square.js +471 -339
- package/dist/square-api.d.ts +1 -0
- package/dist/square-api.js +3 -4
- package/dist/square-core.d.ts +3 -2
- package/dist/square-domain.d.ts +12 -5
- package/dist/square-domain.js +163 -7
- package/dist/square-facade.d.ts +14 -10
- package/dist/square-index.d.ts +24 -0
- package/dist/square-index.js +131 -0
- package/dist/square-legacy-framing.d.ts +9 -0
- package/dist/square-legacy-framing.js +56 -0
- package/dist/square-migration-host-ledger.d.ts +26 -0
- package/dist/square-migration-host-ledger.js +168 -0
- package/dist/square-migration.d.ts +5 -5
- package/dist/square-migration.js +373 -71
- package/dist/square-sqlite.d.ts +9 -9
- package/dist/square-sqlite.js +329 -79
- package/dist/square-stdio.d.ts +1 -1
- package/dist/square-stdio.js +12 -7
- package/dist/square-tools.js +29 -2
- package/dist/stdio-server.js +4 -2
- package/dist/stream.d.ts +3 -2
- package/dist/stream.js +9 -10
- package/dist/views.d.ts +9 -14
- package/dist/views.js +50 -70
- package/dist/wakes.d.ts +6 -5
- package/dist/wakes.js +20 -17
- package/guides/application-facade-delivery-evidence.md +1 -1
- package/guides/claude-native.md +9 -9
- package/guides/mcp.md +1 -1
- package/guides/migration.md +107 -67
- package/guides/row-storage-design.md +1 -1
- package/guides/square-api.md +61 -28
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/hooks/register.js +1 -1
- package/plugins/claude/skills/square/SKILL.md +1 -1
- package/plugins/claude/types.d.ts +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +170 -118
- package/plugins/opencode/index.ts +12 -30
- package/plugins/pi/square-pi.js +55 -17
- package/skills/square/SKILL.md +1 -1
- package/dist/cli/ledger-command.d.ts +0 -12
- package/dist/cli/ledger-command.js +0 -72
- package/dist/host-ledger-file-adapter.d.ts +0 -47
- package/dist/host-ledger-file-adapter.js +0 -384
- package/dist/host-ledger-migration.d.ts +0 -27
- package/dist/host-ledger-migration.js +0 -264
- package/dist/host-ledger-root.d.ts +0 -2
- package/dist/host-ledger-root.js +0 -9
- package/dist/host-ledger-sqlite.d.ts +0 -65
- package/dist/host-ledger-sqlite.js +0 -368
- package/dist/host-ledger.d.ts +0 -195
- package/dist/host-ledger.js +0 -1
- package/dist/ledger-maintenance.d.ts +0 -30
- package/dist/ledger-maintenance.js +0 -110
- package/dist/open-square.d.ts +0 -38
- package/dist/open-square.js +0 -81
- package/dist/participant-host.d.ts +0 -23
- package/dist/participant-host.js +0 -92
- package/dist/registry.d.ts +0 -55
- package/dist/registry.js +0 -123
- package/dist/routes.d.ts +0 -39
- package/dist/routes.js +0 -99
- package/dist/square-actions.d.ts +0 -56
- package/dist/square-actions.js +0 -353
- package/dist/square-projections.d.ts +0 -31
- package/dist/square-projections.js +0 -64
- package/dist/wake-attempts.d.ts +0 -10
- package/dist/wake-attempts.js +0 -39
- package/dist/wake-eligibility.d.ts +0 -53
- package/dist/wake-eligibility.js +0 -123
- package/dist/wake-evidence.d.ts +0 -33
- package/dist/wake-evidence.js +0 -32
- package/guides/host-ledger.md +0 -58
- package/guides/indexed-host-ledger-performance.md +0 -39
|
@@ -1,59 +1,70 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { nameKey } from '../../model.js';
|
|
4
|
-
import { nativePendingPreview, observeSessionPending, sessionInbox } from '../../inbox.js';
|
|
5
|
-
import { hostLedgerForEnv } from '../../registry.js';
|
|
6
|
-
import { openSquare } from '../../open-square.js';
|
|
7
|
-
import { closeOpenSquare } from '../../open-square.js';
|
|
8
|
-
import { presentationSuppressesWake, projectPresentationEvidence } from '../../square-projections.js';
|
|
9
|
-
import { presentPending } from '../../presentation-operations.js';
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createArtifactPort } from '../../artifact.js';
|
|
10
3
|
import { deliverPending } from '../../delivery-operations.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
4
|
+
import { nativePendingPreview, observeSessionPending, projectSessionSeats } from '../../inbox.js';
|
|
5
|
+
import { automaticWakeEligible, presentationBlocksWake } from '../../delivery.js';
|
|
6
|
+
import { sameBody, type Seat } from '../../model.js';
|
|
7
|
+
import type { SquareRead, WakeRequest, WakeTransportPort } from '../../ports.js';
|
|
13
8
|
import { formatActivityId, parseActivityId } from '../../square-core.js';
|
|
9
|
+
import { indexedSquareEntries } from '../../square-index.js';
|
|
10
|
+
import { asSeatRef } from '../../seats.js';
|
|
11
|
+
import { presentPending } from '../../presentation-operations.js';
|
|
14
12
|
import { createNativeInputId, sendNativeText, type OpenCodeNativeSession } from '../../packages/agent-delivery/src/opencode-native.js';
|
|
15
13
|
|
|
16
14
|
const routeKind = 'opencode-server' as const;
|
|
17
|
-
|
|
15
|
+
const bodyFor = (sessionId: string) => ({ channel: 'opencode', session: sessionId });
|
|
16
|
+
const payloadHash = (payload: string) => createHash('sha256').update(payload).digest('hex');
|
|
18
17
|
export interface OpenCodeContextMessage { readonly id?: string; readonly role: string; readonly content: readonly { readonly type: string; readonly text?: string }[] }
|
|
19
|
-
const key = (location: string, participant: string, index: number) => `${location}\0${nameKey(participant)}\0${index}`;
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
function preview(facts: SquareRead, location: string, seat: Seat, index: number) {
|
|
20
|
+
return nativePendingPreview(facts, {
|
|
21
|
+
location, routeKind, sessionId: seat.body.session, participant: seat.participant,
|
|
22
|
+
actIndex: index, address: { sessionId: seat.body.session }, now: Date.now(),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function indexedOpenCodeSessions(env: NodeJS.ProcessEnv): Promise<readonly string[]> {
|
|
27
|
+
return [...new Set((await indexedSquareEntries({ channel: 'opencode' }, env)).map((entry) => entry.body.session))];
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
function
|
|
27
|
-
|
|
30
|
+
export async function discoverOpenCodeSeats(sessionId: string, env: NodeJS.ProcessEnv, signal?: AbortSignal) {
|
|
31
|
+
return projectSessionSeats({ body: bodyFor(sessionId), env }, signal);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sameSeat(left: Seat | undefined, right: Seat): boolean {
|
|
35
|
+
return left !== undefined && left.generation === right.generation && sameBody(left.body, right.body);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createOpenCodeWakeTransport(target: OpenCodeNativeSession, lifetime: AbortSignal): WakeTransportPort {
|
|
28
39
|
return {
|
|
29
|
-
probe: async (route) => !lifetime.aborted && route.kind === routeKind
|
|
30
|
-
|
|
40
|
+
probe: async (route) => !lifetime.aborted && route.kind === routeKind
|
|
41
|
+
&& route.sessionId === target.sessionId && route.address.sessionId === target.sessionId,
|
|
42
|
+
async attempt(request: WakeRequest, timeoutMs: number, beforeSend) {
|
|
43
|
+
const index = parseActivityId(request.activity);
|
|
44
|
+
const artifact = request.artifact;
|
|
45
|
+
if (artifact === undefined || request.claimToken === undefined || index === undefined
|
|
46
|
+
|| !sameBody(request.seat.body, bodyFor(target.sessionId))
|
|
47
|
+
|| request.route.kind !== routeKind || request.route.address.sessionId !== target.sessionId) {
|
|
48
|
+
return { outcome: 'not-capable', diagnostic: 'OpenCode attention has no matching artifact claim or session.' };
|
|
49
|
+
}
|
|
31
50
|
const signal = AbortSignal.any([lifetime, AbortSignal.timeout(timeoutMs)]);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (!await ledger.prepareNativeWake(row) || !await (beforeSend?.() ?? Promise.resolve(true)) || signal.aborted
|
|
50
|
-
|| (await square.artifact.read((facts) => preview(facts, owner, index)?.payload, signal)).value !== prepared.payload) return undefined;
|
|
51
|
-
return { sending: sendNativeText(target, prepared.payload, { inputId: row.nativeDelivery.inputId, delivery: 'steer',
|
|
52
|
-
timeoutMs: Math.max(1, deadline - Date.now()), signal }) };
|
|
53
|
-
} finally { await closeOpenSquare(square); }
|
|
54
|
-
}, signal);
|
|
55
|
-
if (!started) return { outcome: 'not-capable', diagnostic: 'OpenCode attention, binding or local delivery generation changed before send.' };
|
|
56
|
-
const result = await started.sending;
|
|
51
|
+
const started = Date.now();
|
|
52
|
+
if (signal.aborted || !await beforeSend())
|
|
53
|
+
return { outcome: 'not-capable', diagnostic: 'OpenCode attention changed before send.' };
|
|
54
|
+
const { value: prepared } = await artifact.read((facts) => preview(facts, request.location, request.seat as Seat, index), signal);
|
|
55
|
+
if (prepared === undefined) return { outcome: 'not-capable', diagnostic: 'OpenCode attention is no longer current.' };
|
|
56
|
+
const inputId = createNativeInputId();
|
|
57
|
+
const correlation = { harness: 'opencode' as const, token: inputId, payloadHash: payloadHash(prepared.payload) };
|
|
58
|
+
if (!await artifact.prepareDelivery({ seat: request.seat, claimToken: request.claimToken, correlation }, signal))
|
|
59
|
+
return { outcome: 'not-capable', diagnostic: 'OpenCode delivery claim is no longer current.' };
|
|
60
|
+
const fresh = await artifact.read((facts) => preview(facts, request.location, request.seat as Seat, index), signal);
|
|
61
|
+
if (fresh.value === undefined || payloadHash(fresh.value.payload) !== correlation.payloadHash)
|
|
62
|
+
return { outcome: 'not-capable', diagnostic: 'OpenCode attention changed before native send.' };
|
|
63
|
+
if (signal.aborted || !await beforeSend())
|
|
64
|
+
return { outcome: 'not-capable', diagnostic: 'OpenCode attention was consumed before send.' };
|
|
65
|
+
const result = await sendNativeText(target, prepared.payload, {
|
|
66
|
+
inputId, delivery: 'steer', timeoutMs: Math.max(1, timeoutMs - (Date.now() - started)), signal,
|
|
67
|
+
});
|
|
57
68
|
if (result.state === 'accepted') return { outcome: 'accepted' };
|
|
58
69
|
if (result.state === 'unavailable') return { outcome: 'failed', unavailable: true, retainRoute: true, signature: result.code };
|
|
59
70
|
return { outcome: 'unknown', signature: result.code };
|
|
@@ -61,89 +72,130 @@ function createOpenCodeWakeTransport(target: OpenCodeNativeSession, env: NodeJS.
|
|
|
61
72
|
};
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
function textMessage(message: OpenCodeContextMessage): string | undefined {
|
|
76
|
+
if (message.role !== 'user' || message.id === undefined || !message.content.every((part) => part.type === 'text' && typeof part.text === 'string')) return undefined;
|
|
77
|
+
return message.content.map((part) => part.text!).join('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The exact prepared native message can authorize artifact-backed presentation once. */
|
|
65
81
|
export async function observeOpenCodeContext(sessionId: string, messages: readonly OpenCodeContextMessage[], env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
|
|
66
|
-
const
|
|
67
|
-
|
|
82
|
+
const observed = new Map(messages.flatMap((message) => {
|
|
83
|
+
const text = textMessage(message);
|
|
84
|
+
return text === undefined ? [] : [[message.id!, payloadHash(text)] as const];
|
|
85
|
+
}));
|
|
86
|
+
if (observed.size === 0 || signal.aborted) return;
|
|
87
|
+
for (const { location, seat } of await discoverOpenCodeSeats(sessionId, env, signal)) {
|
|
68
88
|
if (signal.aborted) return;
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
const artifact = createArtifactPort(location, undefined, signal);
|
|
90
|
+
try {
|
|
91
|
+
const { value: matches } = await artifact.read((facts) => {
|
|
92
|
+
if (!sameSeat(facts.seat(seat.participant), seat)) return [];
|
|
93
|
+
return facts.deliveryEvidence(seat.participant, seat.generation).flatMap((evidence) => {
|
|
94
|
+
const token = evidence.correlation?.harness === 'opencode' ? evidence.correlation.token : undefined;
|
|
95
|
+
const hash = token === undefined ? undefined : observed.get(token);
|
|
96
|
+
const index = parseActivityId(evidence.activity);
|
|
97
|
+
if (token === undefined || hash === undefined || evidence.claimToken === undefined || index === undefined
|
|
98
|
+
|| !['dispatching', 'unknown', 'accepted'].includes(evidence.outcome)
|
|
99
|
+
|| evidence.correlation?.payloadHash !== hash) return [];
|
|
100
|
+
const prepared = preview(facts, location, seat, index);
|
|
101
|
+
if (prepared === undefined || payloadHash(prepared.payload) !== hash
|
|
102
|
+
|| presentationBlocksWake(facts.deliveryEvidence(seat.participant, seat.generation, evidence.activity), Date.now())) return [];
|
|
103
|
+
return [{ evidence, index, hash, clipped: prepared.clipped }];
|
|
104
|
+
});
|
|
105
|
+
}, signal);
|
|
106
|
+
for (const { evidence, index, hash, clipped } of matches) {
|
|
107
|
+
if (signal.aborted || evidence.claimToken === undefined) 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
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} finally { await artifact.close(); }
|
|
122
|
+
}
|
|
91
123
|
}
|
|
92
124
|
|
|
93
|
-
/**
|
|
125
|
+
/** The native receiver owns no claims outside the artifact; HOME is used only for discovery. */
|
|
94
126
|
export async function receiveOpenCodePending(target: OpenCodeNativeSession, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
const refreshExclusions = async () => {
|
|
99
|
-
excluded.clear();
|
|
100
|
-
const owners = await ledger.listPresence({ session: target.sessionId });
|
|
101
|
-
for (const row of await ledger.listEvidence({ kind: 'wake', session: target.sessionId })) {
|
|
102
|
-
const index = parseActivityId(row.activity);
|
|
103
|
-
const current = owners.some((owner) => owner.location === row.location && nameKey(owner.participant) === nameKey(row.participant) && owner.epoch === row.nativeDelivery?.epoch);
|
|
104
|
-
if (index !== undefined && (['accepted', 'unknown'].includes(row.outcome) || row.outcome === 'dispatching' && current && row.nativeDelivery?.harness === 'opencode')) excluded.add(key(row.location, row.participant, index));
|
|
105
|
-
}
|
|
106
|
-
for (const membership of await sessionInbox(target.sessionId, env, signal)) {
|
|
107
|
-
const owner = owners.find((binding) => binding.location === membership.squarePath && nameKey(binding.participant) === nameKey(membership.name));
|
|
108
|
-
for (const note of membership.notifications) if (note.actIndex <= (owner?.cancelledThrough ?? -1)) excluded.add(key(membership.squarePath, membership.name, note.actIndex));
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
const transport = createWakeTransport([], ledger, Date.now, { [routeKind]: createOpenCodeWakeTransport(target, env, signal) });
|
|
127
|
+
const observer = await observeSessionPending(bodyFor(target.sessionId), env);
|
|
128
|
+
const transport = createOpenCodeWakeTransport(target, signal);
|
|
129
|
+
let waiting: ReturnType<typeof observer.wait> | undefined;
|
|
112
130
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const square = await openSquare(membership.squarePath, { hostLedger: ledger, env, signal });
|
|
131
|
+
while (!signal.aborted) {
|
|
132
|
+
const armed = new Promise<boolean>((resolve) => {
|
|
133
|
+
waiting = observer.wait(Infinity, { signal, skipImmediate: true, onChangeArmed: resolve });
|
|
134
|
+
void waiting.catch(() => resolve(false));
|
|
135
|
+
});
|
|
136
|
+
if (!await armed) { await waiting; break; }
|
|
137
|
+
for (const membership of await observer.read({ signal })) {
|
|
138
|
+
const { location, seat } = membership;
|
|
139
|
+
if (signal.aborted) return;
|
|
140
|
+
const artifact = createArtifactPort(location, undefined, signal);
|
|
124
141
|
try {
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
142
|
+
const { value: notifications } = await artifact.read((facts) => {
|
|
143
|
+
if (!sameSeat(facts.seat(seat.participant), seat)) return [];
|
|
144
|
+
return membership.notifications.filter((note) => automaticWakeEligible(
|
|
145
|
+
facts.deliveryEvidence(seat.participant, seat.generation, formatActivityId(note.actIndex)), Date.now()));
|
|
146
|
+
}, signal);
|
|
147
|
+
for (const note of notifications) {
|
|
148
|
+
if (signal.aborted) return;
|
|
149
|
+
await deliverPending({ artifact, transport, location, activity: formatActivityId(note.actIndex), now: Date.now() });
|
|
128
150
|
}
|
|
129
|
-
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (signal.aborted) throw signal.reason ?? error;
|
|
153
|
+
// A candidate that became unreadable cannot authorize sending; the shared observer
|
|
154
|
+
// keeps its subscription and will reconsider the artifact after repair.
|
|
155
|
+
} finally { await artifact.close(); }
|
|
130
156
|
}
|
|
131
|
-
await
|
|
132
|
-
|
|
157
|
+
await waiting;
|
|
158
|
+
waiting = undefined;
|
|
133
159
|
}
|
|
134
|
-
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (!signal.aborted) throw error;
|
|
162
|
+
} finally {
|
|
163
|
+
observer.close();
|
|
164
|
+
await waiting?.catch(() => {});
|
|
165
|
+
}
|
|
135
166
|
}
|
|
136
167
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
168
|
+
/** Publish the native address into current artifact seats; the legacy route projection is not read. */
|
|
169
|
+
export async function publishOpenCodeAddress(sessionId: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<boolean> {
|
|
170
|
+
const seats = await discoverOpenCodeSeats(sessionId, env, signal);
|
|
171
|
+
for (const { location, seat } of seats) {
|
|
172
|
+
if (signal.aborted) return false;
|
|
173
|
+
const artifact = createArtifactPort(location, undefined, signal);
|
|
174
|
+
try {
|
|
175
|
+
await artifact.transactAs({ kind: 'participant', seat: asSeatRef(seat) }, (facts) =>
|
|
176
|
+
facts.updateWakeAddress(asSeatRef(seat), { kind: routeKind, address: { sessionId } }));
|
|
177
|
+
} finally { await artifact.close(); }
|
|
178
|
+
}
|
|
179
|
+
return seats.length > 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function cancelOpenCodeDeliveries(sessionId: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
|
|
183
|
+
for (const { location, seat } of await discoverOpenCodeSeats(sessionId, env, signal)) {
|
|
184
|
+
if (signal.aborted) return;
|
|
185
|
+
const artifact = createArtifactPort(location, undefined, signal);
|
|
186
|
+
try {
|
|
187
|
+
const { value: through } = await artifact.read((facts) => facts.header().lastActIndex, signal);
|
|
188
|
+
if (through >= 0) await artifact.cancelDeliveries(asSeatRef(seat), through, signal);
|
|
189
|
+
} finally { await artifact.close(); }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function endOpenCodeSession(sessionId: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
|
|
194
|
+
for (const { location, seat } of await discoverOpenCodeSeats(sessionId, env, signal)) {
|
|
195
|
+
if (signal.aborted) return;
|
|
196
|
+
const { Square } = await import('../../square-api.js');
|
|
197
|
+
const square = await Square.at({ path: location, env, body: seat.body, participant: seat.participant });
|
|
198
|
+
try { await square.endOwnedSession(seat.participant, sessionId, seat.generation, seat.body); }
|
|
199
|
+
finally { await square.close(); }
|
|
200
|
+
}
|
|
149
201
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
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, type OpenCodeNativeSession, type OpenCodeSessionCapability } from '../../packages/agent-delivery/src/opencode-native.js';
|
|
8
|
-
import { observeOpenCodeContext,
|
|
4
|
+
import { cancelOpenCodeDeliveries, endOpenCodeSession, indexedOpenCodeSessions, observeOpenCodeContext, publishOpenCodeAddress, receiveOpenCodePending, type OpenCodeContextMessage } from './delivery.js';
|
|
9
5
|
|
|
10
6
|
interface OpenCodePluginContext {
|
|
11
7
|
readonly app: { readonly version: string };
|
|
@@ -24,7 +20,7 @@ export default {
|
|
|
24
20
|
async setup(ctx: OpenCodePluginContext): Promise<() => Promise<void>> {
|
|
25
21
|
if (ctx.app.version !== '2.0.20') return async () => {};
|
|
26
22
|
const env = { ...process.env };
|
|
27
|
-
const
|
|
23
|
+
const index = await observeSquareIndex(env);
|
|
28
24
|
const lifetime = new AbortController();
|
|
29
25
|
const receivers = new Map<string, Receiver>(); // Operational handles only; presence owns membership.
|
|
30
26
|
const directory = await canonicalPath(ctx.location.directory);
|
|
@@ -53,18 +49,17 @@ export default {
|
|
|
53
49
|
return target;
|
|
54
50
|
};
|
|
55
51
|
const reconcile = async (activeSession?: string) => {
|
|
56
|
-
const candidates = new Set(
|
|
52
|
+
const candidates = new Set(await indexedOpenCodeSessions(env));
|
|
57
53
|
if (activeSession) candidates.add(activeSession);
|
|
58
54
|
for (const sessionId of candidates) {
|
|
59
55
|
if (lifetime.signal.aborted) return;
|
|
60
56
|
const target = await validate(sessionId);
|
|
61
57
|
if (!target) { stop(sessionId); continue; }
|
|
62
|
-
|
|
63
|
-
if (!await publishOpenCodeRoutes(sessionId, scopedEnv, lifetime.signal)) { stop(sessionId); continue; }
|
|
58
|
+
if (!await publishOpenCodeAddress(sessionId, env, lifetime.signal)) { stop(sessionId); continue; }
|
|
64
59
|
if (receivers.get(sessionId)?.settled === false) continue;
|
|
65
60
|
const abort = new AbortController();
|
|
66
61
|
const signal = AbortSignal.any([lifetime.signal, abort.signal]);
|
|
67
|
-
const receiver: Receiver = { abort, settled: false, running: receiveOpenCodePending(target,
|
|
62
|
+
const receiver: Receiver = { abort, settled: false, running: receiveOpenCodePending(target, env, signal).catch(() => {}).finally(() => { receiver.settled = true; }) };
|
|
68
63
|
receivers.set(sessionId, receiver);
|
|
69
64
|
}
|
|
70
65
|
for (const sessionId of receivers.keys()) if (!candidates.has(sessionId)) stop(sessionId);
|
|
@@ -73,23 +68,12 @@ export default {
|
|
|
73
68
|
if (!receivers.has(sessionId)) return;
|
|
74
69
|
// Stop synchronously; persist the cutoff before allowing reconciliation to start fresh work.
|
|
75
70
|
stop(sessionId);
|
|
76
|
-
await
|
|
77
|
-
for (const owner of (await ledger.listPresence({ session: sessionId })).filter((binding) => binding.channel === 'opencode')) {
|
|
78
|
-
const square = await openSquare(owner.location, { hostLedger: ledger, env, signal: lifetime.signal });
|
|
79
|
-
try { await ledger.suppressPresence(owner, (await square.artifact.read((facts) => facts.header().nextActIndex - 1, lifetime.signal)).value); }
|
|
80
|
-
finally { await closeOpenSquare(square); }
|
|
81
|
-
}
|
|
82
|
-
}, lifetime.signal);
|
|
71
|
+
await cancelOpenCodeDeliveries(sessionId, env, lifetime.signal);
|
|
83
72
|
await reconcile();
|
|
84
73
|
};
|
|
85
74
|
const end = async (sessionId: string) => {
|
|
86
75
|
stop(sessionId);
|
|
87
|
-
|
|
88
|
-
for (const owner of memberships) {
|
|
89
|
-
const square = await Square.at({ path: owner.location, hostLedger: ledger, env: operationEnv('opencode', sessionId, env) });
|
|
90
|
-
try { await square.endOwnedSession(owner.participant, sessionId, owner.epoch); }
|
|
91
|
-
finally { await square.close(); }
|
|
92
|
-
}
|
|
76
|
+
await endOpenCodeSession(sessionId, env, lifetime.signal);
|
|
93
77
|
};
|
|
94
78
|
|
|
95
79
|
await ctx.session.hook('prompt', (event) => coordinate(() => reconcile(event.sessionID)));
|
|
@@ -97,16 +81,14 @@ export default {
|
|
|
97
81
|
await coordinate(() => reconcile(event.sessionID));
|
|
98
82
|
const receiver = receivers.get(event.sessionID);
|
|
99
83
|
if (!receiver || lifetime.signal.aborted) return;
|
|
100
|
-
await observeOpenCodeContext(event.sessionID, event.messages,
|
|
84
|
+
await observeOpenCodeContext(event.sessionID, event.messages, env,
|
|
101
85
|
AbortSignal.any([lifetime.signal, receiver.abort.signal])).catch(() => {});
|
|
102
86
|
});
|
|
103
|
-
const changes = await ledger.observeChanges();
|
|
104
|
-
changes.evidence.close(); // Manual joins activate an idle receiver; evidence already has a session observer.
|
|
105
87
|
const presenceLoop = (async () => {
|
|
106
88
|
while (!lifetime.signal.aborted) {
|
|
107
|
-
const baseline = await
|
|
89
|
+
const baseline = await index.read(lifetime.signal);
|
|
108
90
|
await coordinate(() => reconcile());
|
|
109
|
-
await
|
|
91
|
+
await index.changed(baseline, Infinity, lifetime.signal);
|
|
110
92
|
}
|
|
111
93
|
})().catch(() => {});
|
|
112
94
|
const eventLoop = (async () => {
|
|
@@ -124,7 +106,7 @@ export default {
|
|
|
124
106
|
// Do not await any native prompt in setup: plugin activation itself is lazy.
|
|
125
107
|
return async () => {
|
|
126
108
|
lifetime.abort();
|
|
127
|
-
|
|
109
|
+
index.close();
|
|
128
110
|
const tasks = [presenceLoop, eventLoop, coordinating, ...[...receivers.keys()].map(stop)];
|
|
129
111
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
130
112
|
try { await Promise.race([Promise.allSettled(tasks), new Promise<void>((resolve) => { timer = setTimeout(resolve, 3000); })]); }
|
package/plugins/pi/square-pi.js
CHANGED
|
@@ -8,7 +8,7 @@ import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolFailure, squareToolFa
|
|
|
8
8
|
import { presentPendingAtBoundary, renderPendingAtBoundary } from '../../dist/boundary-presentation.js';
|
|
9
9
|
import { operationEnv } from '../../dist/participant-identity.js';
|
|
10
10
|
import { endSessionMemberships } from '../../dist/session-lifecycle.js';
|
|
11
|
-
import {
|
|
11
|
+
import { projectSessionSeats, observeSessionPending, pendingKey } from '../../dist/inbox.js';
|
|
12
12
|
import { previewAttentionBody, renderAttentionDescription, renderAttentionFull } from '../../dist/attention-presentation.js';
|
|
13
13
|
import { formatActivityId } from '../../dist/square-core.js';
|
|
14
14
|
import { createPiReceiver, sendPiMessage } from '../../dist/packages/agent-delivery/src/pi.js';
|
|
@@ -26,7 +26,7 @@ export function pendingInbox(inbox) {
|
|
|
26
26
|
|
|
27
27
|
export function inboxKeys(inbox) {
|
|
28
28
|
return pendingInbox(inbox).flatMap((item) => item.notifications.map((note) =>
|
|
29
|
-
|
|
29
|
+
pendingKey(item, note.actIndex)
|
|
30
30
|
));
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -41,9 +41,9 @@ function framePiMessage(content) {
|
|
|
41
41
|
/** Renderer-only card facts for Pi and Paseo; Pi never sends `details` to the model. */
|
|
42
42
|
export function pendingCardDetails(pending) {
|
|
43
43
|
const items = pending.flatMap((membership) => membership.notifications.map((note) => ({
|
|
44
|
-
square: path.basename(membership.
|
|
44
|
+
square: path.basename(membership.location, '.square'),
|
|
45
45
|
actor: note.actor,
|
|
46
|
-
recipient: membership.
|
|
46
|
+
recipient: membership.seat.participant,
|
|
47
47
|
route: note.route,
|
|
48
48
|
act: formatActivityId(note.actIndex),
|
|
49
49
|
preview: previewAttentionBody(note.body),
|
|
@@ -52,9 +52,9 @@ export function pendingCardDetails(pending) {
|
|
|
52
52
|
})));
|
|
53
53
|
if (items.length === 0) return undefined;
|
|
54
54
|
const fullText = pending.flatMap((membership) => membership.notifications.map((note) => renderAttentionFull({
|
|
55
|
-
squarePath: membership.
|
|
55
|
+
squarePath: membership.location,
|
|
56
56
|
actIndex: note.actIndex,
|
|
57
|
-
recipient: membership.
|
|
57
|
+
recipient: membership.seat.participant,
|
|
58
58
|
...note,
|
|
59
59
|
}))).join('\n\n');
|
|
60
60
|
return { items, fullText };
|
|
@@ -152,9 +152,11 @@ export default function squarePiExtension(pi) {
|
|
|
152
152
|
namespace: { name: 'square', description: 'Participate in a shared public square.', instructions: SQUARE_TOOL_INSTRUCTIONS },
|
|
153
153
|
async execute(_id, args, signal, _onUpdate, ctx) {
|
|
154
154
|
try {
|
|
155
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
155
156
|
const square = await Square.at({
|
|
156
157
|
cwd: ctx.cwd,
|
|
157
|
-
env: operationEnv('pi',
|
|
158
|
+
env: operationEnv('pi', sessionId, process.env),
|
|
159
|
+
body: { channel: 'pi', session: sessionId },
|
|
158
160
|
path: args.location,
|
|
159
161
|
participant: args.as,
|
|
160
162
|
}, { signal });
|
|
@@ -164,7 +166,7 @@ export default function squarePiExtension(pi) {
|
|
|
164
166
|
return { content: [{ type: 'text', text: tool.render(data, args) ?? '' }], details: data, structuredContent: data };
|
|
165
167
|
} catch (error) {
|
|
166
168
|
const data = { error: squareToolFailure(error, signal) };
|
|
167
|
-
return { isError: true, content: [{ type: 'text', text: squareToolFailureText(data.error, { location: args.location ?? process.env.SQUARE_LOCATION, participant: args.as
|
|
169
|
+
return { isError: true, content: [{ type: 'text', text: squareToolFailureText(data.error, { location: args.location ?? process.env.SQUARE_LOCATION, participant: args.as }) }], details: data, structuredContent: data };
|
|
168
170
|
}
|
|
169
171
|
},
|
|
170
172
|
});
|
|
@@ -188,10 +190,14 @@ export default function squarePiExtension(pi) {
|
|
|
188
190
|
let currentRunSignal;
|
|
189
191
|
let detachRunAbort;
|
|
190
192
|
let cancelledRun = false;
|
|
193
|
+
let pendingCancellations = new Map();
|
|
194
|
+
const cancellationFailures = new Set();
|
|
191
195
|
|
|
192
196
|
const cancelRunDelivery = () => {
|
|
197
|
+
if (cancelledRun) return;
|
|
193
198
|
cancelledRun = true;
|
|
194
199
|
for (const key of observedPending) handledPending.add(key);
|
|
200
|
+
persistSessionCancellation(sessionId);
|
|
195
201
|
generation += 1;
|
|
196
202
|
stopWatcher();
|
|
197
203
|
failAcks(new Error('Pi notification delivery cancelled'));
|
|
@@ -245,6 +251,40 @@ export default function squarePiExtension(pi) {
|
|
|
245
251
|
// Pi owns PI_SESSION_ID and several SDK sessions may coexist in one process.
|
|
246
252
|
const sessionEnv = (id) => ({ ...process.env, ...(id === undefined ? {} : { PI_SESSION_ID: id }) });
|
|
247
253
|
|
|
254
|
+
const persistSessionCancellation = (id) => {
|
|
255
|
+
if (id === undefined) return Promise.resolve();
|
|
256
|
+
const previous = pendingCancellations.get(id) ?? Promise.resolve();
|
|
257
|
+
let operation;
|
|
258
|
+
operation = previous.then(async () => {
|
|
259
|
+
const body = { channel: 'pi', session: id };
|
|
260
|
+
const env = sessionEnv(id);
|
|
261
|
+
const seats = await projectSessionSeats({ body, env });
|
|
262
|
+
for (const { location, seat } of seats) {
|
|
263
|
+
const square = await Square.at({ path: location, env, body, participant: seat.participant });
|
|
264
|
+
try { await square.cancelDeliveries(seat.participant, seat.generation); }
|
|
265
|
+
finally { await square.close(); }
|
|
266
|
+
}
|
|
267
|
+
}).then(() => {
|
|
268
|
+
cancellationFailures.delete(id);
|
|
269
|
+
}, () => {
|
|
270
|
+
cancellationFailures.add(id);
|
|
271
|
+
}).finally(() => {
|
|
272
|
+
if (pendingCancellations.get(id) === operation) pendingCancellations.delete(id);
|
|
273
|
+
});
|
|
274
|
+
pendingCancellations.set(id, operation);
|
|
275
|
+
return operation;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const waitForSessionCancellation = async (id) => {
|
|
279
|
+
if (id === undefined) return true;
|
|
280
|
+
while (true) {
|
|
281
|
+
const pending = pendingCancellations.get(id);
|
|
282
|
+
if (pending !== undefined) await pending;
|
|
283
|
+
if (pendingCancellations.get(id) !== pending) continue;
|
|
284
|
+
return !cancellationFailures.has(id);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
248
288
|
// Aborting releases native landing waits; retain cleanup until the observer
|
|
249
289
|
// has closed, including when cancellation precedes a lifecycle hook.
|
|
250
290
|
const stopWatcher = () => {
|
|
@@ -267,7 +307,7 @@ export default function squarePiExtension(pi) {
|
|
|
267
307
|
const wake = async (token, signal) => {
|
|
268
308
|
const boundSessionId = sessionId;
|
|
269
309
|
const env = sessionEnv(boundSessionId);
|
|
270
|
-
const observer = await observeSessionPending(boundSessionId, env);
|
|
310
|
+
const observer = await observeSessionPending({ channel: 'pi', session: boundSessionId }, env);
|
|
271
311
|
try {
|
|
272
312
|
if (signal.aborted || token !== generation) return;
|
|
273
313
|
const armDeferredRetry = () => {
|
|
@@ -321,7 +361,7 @@ export default function squarePiExtension(pi) {
|
|
|
321
361
|
let keepDeferred = false;
|
|
322
362
|
try {
|
|
323
363
|
await presentPendingAtBoundary(
|
|
324
|
-
boundSessionId,
|
|
364
|
+
{ channel: 'pi', session: boundSessionId },
|
|
325
365
|
async (content) => {
|
|
326
366
|
if (signal.aborted || currentRunSignal?.aborted) throw new Error('Pi notification delivery cancelled');
|
|
327
367
|
// One card for the human: a compact heading, expandable to the full body.
|
|
@@ -384,7 +424,8 @@ export default function squarePiExtension(pi) {
|
|
|
384
424
|
sessionId = ctx.sessionManager.getSessionId();
|
|
385
425
|
const token = generation;
|
|
386
426
|
await stopped;
|
|
387
|
-
|
|
427
|
+
await waitForSessionCancellation(sessionId);
|
|
428
|
+
if (token !== generation || cancellationFailures.has(sessionId)) return;
|
|
388
429
|
watcherAbort = new AbortController();
|
|
389
430
|
const signal = watcherAbort.signal;
|
|
390
431
|
watcher = wake(token, signal).catch(() => undefined);
|
|
@@ -438,13 +479,9 @@ export default function squarePiExtension(pi) {
|
|
|
438
479
|
currentRunSignal = undefined;
|
|
439
480
|
if (!cancelledRun || sessionId === undefined) return;
|
|
440
481
|
const token = generation;
|
|
441
|
-
// Cancel the entire pending batch, including entries not yet sent to Pi.
|
|
442
|
-
// This is local suppression, never evidence that the model saw the activity.
|
|
443
482
|
await stopWatcher();
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (token !== generation || sessionId === undefined) return;
|
|
447
|
-
for (const key of inboxKeys(cancelled)) handledPending.add(key);
|
|
483
|
+
await waitForSessionCancellation(sessionId);
|
|
484
|
+
if (token !== generation || sessionId === undefined || cancellationFailures.has(sessionId)) return;
|
|
448
485
|
cancelledRun = false;
|
|
449
486
|
watcherAbort = new AbortController();
|
|
450
487
|
watcher = wake(token, watcherAbort.signal).catch(() => undefined);
|
|
@@ -464,6 +501,7 @@ export default function squarePiExtension(pi) {
|
|
|
464
501
|
activeTurn = undefined;
|
|
465
502
|
currentRunSignal = undefined;
|
|
466
503
|
await stopped;
|
|
504
|
+
await waitForSessionCancellation(endedId);
|
|
467
505
|
if (endedId) await endSessionMemberships('pi', endedId, endedEnv).catch(() => undefined);
|
|
468
506
|
});
|
|
469
507
|
}
|
package/skills/square/SKILL.md
CHANGED
|
@@ -10,7 +10,7 @@ Bring a thought, a question, an objection, or a gesture. Leave private tool chat
|
|
|
10
10
|
|
|
11
11
|
This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The operations are the same either way.
|
|
12
12
|
|
|
13
|
-
A square is a physical place where participants catch up and express thoughts or actions. Every square needs a location and
|
|
13
|
+
A square is a physical place where participants catch up and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `--location` (or `SQUARE_LOCATION`) and an explicit `--as <name>` when joining. Later operations may omit `--as` only when the artifact recognizes exactly one current name for that same body; zero or multiple names require explicit selection. A name never grants custody.
|
|
14
14
|
|
|
15
15
|
## The rhythm
|
|
16
16
|
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { type LedgerMaintenanceReport } from '../ledger-maintenance.js';
|
|
2
|
-
import { type HostLedgerReport } from '../host-ledger-migration.js';
|
|
3
|
-
import { type CommandSpec } from './context.js';
|
|
4
|
-
type LedgerIntent = {
|
|
5
|
-
readonly action: 'verify' | 'migrate';
|
|
6
|
-
} | {
|
|
7
|
-
readonly action: 'gc';
|
|
8
|
-
readonly dryRun: boolean;
|
|
9
|
-
};
|
|
10
|
-
type LedgerResult = HostLedgerReport | LedgerMaintenanceReport;
|
|
11
|
-
export declare const ledgerCommand: CommandSpec<LedgerIntent, LedgerResult>;
|
|
12
|
-
export {};
|