@astrosheep/square 0.3.106 → 0.4.1
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/activity-feed.js +1 -1
- package/dist/artifact-store.d.ts +41 -0
- package/dist/artifact-store.js +272 -0
- package/dist/artifact.d.ts +6 -12
- package/dist/artifact.js +43 -344
- package/dist/attention-presentation.d.ts +13 -7
- package/dist/attention-presentation.js +29 -68
- package/dist/boundary-presentation.d.ts +3 -16
- package/dist/boundary-presentation.js +29 -109
- package/dist/card-presentation.d.ts +1 -0
- package/dist/card-presentation.js +2 -0
- package/dist/catch-decisions.d.ts +2 -2
- package/dist/catch-decisions.js +7 -3
- package/dist/catch-selection.d.ts +8 -0
- package/dist/catch-selection.js +5 -0
- package/dist/claude-delivery.d.ts +14 -12
- package/dist/claude-delivery.js +42 -142
- package/dist/claude-mcp-context.js +2 -2
- package/dist/claude-mod.js +28 -140
- package/dist/claude-transport.d.ts +5 -0
- package/dist/claude-transport.js +34 -0
- package/dist/cli/context.d.ts +2 -2
- package/dist/cli/context.js +7 -10
- package/dist/cli/maintenance-commands.d.ts +0 -4
- package/dist/cli/maintenance-commands.js +2 -29
- package/dist/cli/observation-commands.d.ts +2 -0
- package/dist/cli/observation-commands.js +12 -2
- package/dist/cli/program.js +1 -1
- package/dist/cli/registry.js +4 -3
- package/dist/codex-address.d.ts +2 -2
- package/dist/codex-boundary-state.d.ts +1 -5
- package/dist/codex-boundary-state.js +0 -4
- package/dist/codex-hook.d.ts +7 -10
- package/dist/codex-hook.js +69 -33
- package/dist/codex-queue.d.ts +10 -11
- package/dist/codex-queue.js +32 -34
- package/dist/codex-watch.d.ts +2 -0
- package/dist/codex-watch.js +100 -0
- package/dist/decisions.js +3 -3
- package/dist/delivery-deferral.d.ts +1 -19
- package/dist/delivery-deferral.js +1 -29
- package/dist/delivery-frame.d.ts +7 -0
- package/dist/delivery-frame.js +11 -0
- package/dist/delivery-ledger-sqlite.d.ts +97 -0
- package/dist/delivery-ledger-sqlite.js +496 -0
- package/dist/delivery-ledger.d.ts +196 -0
- package/dist/delivery-ledger.js +268 -0
- package/dist/delivery-lifetime.d.ts +6 -0
- package/dist/delivery-lifetime.js +45 -0
- package/dist/harness-links.js +61 -7
- package/dist/harness.js +12 -17
- package/dist/help.js +3 -6
- package/dist/http-delivery.d.ts +5 -9
- package/dist/http-delivery.js +20 -79
- package/dist/inbox.d.ts +6 -57
- package/dist/inbox.js +24 -380
- package/dist/index.d.ts +7 -4
- package/dist/index.js +5 -2
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +40 -70
- package/dist/message.d.ts +7 -0
- package/dist/message.js +23 -0
- package/dist/model.d.ts +15 -91
- package/dist/model.js +2 -140
- package/dist/neutral.d.ts +72 -0
- package/dist/neutral.js +27 -0
- package/dist/notifications.d.ts +24 -26
- package/dist/notifications.js +39 -143
- package/dist/operation-control.d.ts +4 -0
- package/dist/operation-control.js +20 -0
- package/dist/operation-presentation.d.ts +1 -2
- package/dist/operation-presentation.js +1 -7
- package/dist/owner.d.ts +2 -2
- package/dist/owner.js +1 -1
- package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
- package/dist/packages/agent-delivery/src/claude-native.js +5 -0
- package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
- package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
- package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
- package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
- package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
- package/dist/packages/agent-delivery/src/codex.js +41 -4
- package/dist/packages/agent-delivery/src/index.d.ts +3 -1
- package/dist/packages/agent-delivery/src/index.js +1 -1
- package/dist/packages/agent-delivery/src/paseo.js +11 -2
- package/dist/packages/agent-delivery/src/types.d.ts +1 -0
- package/dist/paseo-delivery.d.ts +5 -5
- package/dist/paseo-delivery.js +19 -99
- package/dist/paseo.d.ts +1 -1
- package/dist/paseo.js +1 -1
- package/dist/plugins/opencode/index.d.ts +5 -3
- package/dist/plugins/opencode/index.js +99 -21
- package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
- package/dist/plugins/opencode-shared/delivery.js +109 -0
- package/dist/plugins/opencode-v1/index.d.ts +28 -0
- package/dist/plugins/opencode-v1/index.js +41 -0
- package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
- package/dist/plugins/opencode-v1/runtime.js +152 -0
- package/dist/plugins/opencode-v1/transport.d.ts +13 -0
- package/dist/plugins/opencode-v1/transport.js +50 -0
- package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
- package/dist/plugins/paseo/shared/card-presentation.js +7 -0
- package/dist/ports.d.ts +33 -214
- package/dist/presentation.d.ts +5 -5
- package/dist/presentation.js +1 -0
- package/dist/runtime.js +2 -2
- package/dist/search.js +1 -1
- package/dist/session-lifecycle.d.ts +8 -7
- package/dist/session-lifecycle.js +40 -42
- package/dist/sqlite-framing.d.ts +8 -0
- package/dist/sqlite-framing.js +31 -0
- package/dist/sqlite-square.d.ts +16 -26
- package/dist/sqlite-square.js +199 -262
- package/dist/sqlite-values.d.ts +21 -0
- package/dist/sqlite-values.js +78 -0
- package/dist/square-api.d.ts +1 -1
- package/dist/square-api.js +5 -8
- package/dist/square-core.d.ts +0 -1
- package/dist/square-domain.d.ts +12 -10
- package/dist/square-domain.js +105 -223
- package/dist/square-facade.d.ts +25 -76
- package/dist/square-location.d.ts +6 -1
- package/dist/square-location.js +1 -1
- package/dist/square-post.d.ts +53 -0
- package/dist/square-post.js +4 -0
- package/dist/square-posting.d.ts +13 -0
- package/dist/square-posting.js +174 -0
- package/dist/square-producer-sqlite.d.ts +11 -0
- package/dist/square-producer-sqlite.js +210 -0
- package/dist/square-receive.d.ts +2 -3
- package/dist/square-sqlite.d.ts +33 -42
- package/dist/square-sqlite.js +236 -768
- package/dist/square-tools.d.ts +8 -0
- package/dist/square-tools.js +39 -0
- package/dist/square-values.d.ts +27 -0
- package/dist/square-values.js +90 -0
- package/dist/views.d.ts +6 -6
- package/dist/views.js +14 -9
- package/dist/wake-sink.d.ts +4 -11
- package/dist/wake-sink.js +8 -32
- package/dist/wake-transports.d.ts +4 -0
- package/dist/wake-transports.js +11 -0
- package/examples/symmetric-domains.mjs +29 -249
- package/guides/change-waits.md +13 -53
- package/guides/claude-native.md +19 -172
- package/guides/codex-delivery.md +55 -78
- package/guides/delivery-primitives.md +48 -207
- package/guides/operation-cancellation.md +2 -5
- package/guides/participant-ownership.md +25 -89
- package/guides/pi-native-delivery.md +19 -56
- package/guides/pi-tools.md +6 -8
- package/guides/row-runtime-delivery-evidence.md +2 -0
- package/guides/row-storage-design.md +19 -44
- package/guides/square-api.md +113 -538
- package/package.json +8 -8
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/hooks/hooks.json +1 -1
- package/plugins/claude/{hooks/register.js → mods/square.js} +4 -7
- package/plugins/claude/types.d.ts +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/hooks/hooks.json +11 -23
- package/plugins/opencode/index.ts +91 -23
- package/plugins/opencode-shared/delivery.ts +91 -0
- package/plugins/opencode-v1/index.ts +36 -0
- package/plugins/opencode-v1/runtime.ts +134 -0
- package/plugins/opencode-v1/transport.ts +41 -0
- package/plugins/paseo/OVERVIEW.md +5 -5
- package/plugins/paseo/README.md +7 -6
- package/plugins/paseo/client/markdown.tsx +5 -5
- package/plugins/paseo/client/square-message.tsx +28 -24
- package/plugins/paseo/client/transform.ts +8 -5
- package/plugins/paseo/shared/card-presentation.ts +16 -0
- package/plugins/paseo/shared/message.ts +6 -11
- package/plugins/pi/square-pi.js +167 -451
- package/dist/channel.d.ts +0 -74
- package/dist/channel.js +0 -92
- package/dist/claude-delivery-status.d.ts +0 -13
- package/dist/claude-delivery-status.js +0 -46
- package/dist/cmd/notify-once.d.ts +0 -2
- package/dist/cmd/notify-once.js +0 -29
- package/dist/delivery-attempts.d.ts +0 -52
- package/dist/delivery-attempts.js +0 -73
- package/dist/delivery-effects.d.ts +0 -139
- package/dist/delivery-effects.js +0 -163
- package/dist/delivery-health.d.ts +0 -51
- package/dist/delivery-health.js +0 -173
- package/dist/delivery-operations.d.ts +0 -9
- package/dist/delivery-operations.js +0 -168
- package/dist/delivery-primitives.d.ts +0 -5
- package/dist/delivery-primitives.js +0 -3
- package/dist/delivery.d.ts +0 -45
- package/dist/delivery.js +0 -106
- package/dist/plugins/opencode/delivery.d.ts +0 -19
- package/dist/plugins/opencode/delivery.js +0 -238
- package/dist/presentation-operations.d.ts +0 -18
- package/dist/presentation-operations.js +0 -86
- package/dist/square-index.d.ts +0 -16
- package/dist/square-index.js +0 -106
- package/dist/square-legacy-framing.d.ts +0 -11
- package/dist/square-legacy-framing.js +0 -64
- package/dist/square-migration-host-ledger.d.ts +0 -26
- package/dist/square-migration-host-ledger.js +0 -169
- package/dist/square-migration.d.ts +0 -30
- package/dist/square-migration.js +0 -599
- package/guides/migration.md +0 -156
- package/plugins/opencode/delivery.ts +0 -211
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrosheep/square",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "A shared public square where agents join, catch activity, express, and step out when done.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,13 +50,9 @@
|
|
|
50
50
|
"ws": "^8.21.3"
|
|
51
51
|
},
|
|
52
52
|
"exports": {
|
|
53
|
-
"./
|
|
54
|
-
"types": "./dist/
|
|
55
|
-
"default": "./dist/
|
|
56
|
-
},
|
|
57
|
-
"./delivery": {
|
|
58
|
-
"types": "./dist/delivery-primitives.d.ts",
|
|
59
|
-
"default": "./dist/delivery-primitives.js"
|
|
53
|
+
"./delivery-ledger": {
|
|
54
|
+
"types": "./dist/delivery-ledger.d.ts",
|
|
55
|
+
"default": "./dist/delivery-ledger.js"
|
|
60
56
|
},
|
|
61
57
|
"./receive": {
|
|
62
58
|
"types": "./dist/receive.d.ts",
|
|
@@ -74,6 +70,10 @@
|
|
|
74
70
|
"types": "./dist/paseo.d.ts",
|
|
75
71
|
"default": "./dist/paseo.js"
|
|
76
72
|
},
|
|
73
|
+
"./opencode-v1": {
|
|
74
|
+
"types": "./dist/plugins/opencode-v1/index.d.ts",
|
|
75
|
+
"default": "./dist/plugins/opencode-v1/index.js"
|
|
76
|
+
},
|
|
77
77
|
"./server": {
|
|
78
78
|
"types": "./dist/plugins/opencode/index.d.ts",
|
|
79
79
|
"default": "./dist/plugins/opencode/index.js"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"modules":["
|
|
1
|
+
{"modules":["../mods/square.js"]}
|
|
@@ -11,15 +11,12 @@ async function bridge($, value, budget = 2800) {
|
|
|
11
11
|
}
|
|
12
12
|
async function snapshot($, id) { return (await $.state.get({ plugin: 'square', key: 'inbox', id })).value; }
|
|
13
13
|
async function cancellation($, id) { return (await $.state.get({ plugin: 'square', key: 'cancelAt', id })).value; }
|
|
14
|
-
function confirmLater($, c, operation, text) {
|
|
14
|
+
function confirmLater($, c, operation, text, refs) {
|
|
15
15
|
// State reads are dispatch snapshots. This ONE fresh timer dispatch is not polling.
|
|
16
16
|
// Reload cancels it; lost evidence stays unknown and is never blindly replayed.
|
|
17
17
|
try { $.clock.after(0, () => {
|
|
18
18
|
(async () => {
|
|
19
|
-
|
|
20
|
-
const old = await snapshot($, c.sessionId);
|
|
21
|
-
const cancelled = await cancellation($, c.sessionId);
|
|
22
|
-
await bridge($, { ...c, cancelAt: cancelled?.at, cancelledBindings: cancelled?.bindings, bindings: old?.bindings, operation, text });
|
|
19
|
+
await bridge($, { ...c, operation, text, refs });
|
|
23
20
|
})().catch(() => undefined);
|
|
24
21
|
}); } catch { /* denied scheduling leaves evidence unknown, never a second next */ }
|
|
25
22
|
}
|
|
@@ -112,10 +109,10 @@ export function register(on) {
|
|
|
112
109
|
const old = await snapshot($, c.sessionId).catch(() => undefined);
|
|
113
110
|
const cancelled = await cancellation($, c.sessionId).catch(() => undefined);
|
|
114
111
|
const guard = await bridge($, { ...c, cancelAt: cancelled?.at, cancelledBindings: cancelled?.bindings, bindings: old?.bindings, operation: 'guard', text: e.text }).catch(() => undefined);
|
|
115
|
-
if (guard
|
|
112
|
+
if (!guard || (guard.recognized && !guard.current)) return { consumed: 'Square delivery is stale, cancelled, or unavailable' };
|
|
116
113
|
// Preserve native policy: hold/refuse reject this gate; never call next twice.
|
|
117
114
|
const result = await next(e);
|
|
118
|
-
if (guard?.current && result.text === e.text) confirmLater($, c, 'admitted', result.text);
|
|
115
|
+
if (guard?.current && result.text === e.text) confirmLater($, c, 'admitted', result.text, guard.refs);
|
|
119
116
|
return result;
|
|
120
117
|
});
|
|
121
118
|
on('session.append', async ($, e, next) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SquareInboxBinding = { location: string; participant: string; session: string; generation?:
|
|
1
|
+
export type SquareInboxBinding = { location: string; participant: string; session: string; generation?: string };
|
|
2
2
|
export type SquareInboxCoordinate = { sessionId: string; cwd: string; version: string; endpoint?: string };
|
|
3
3
|
export type SquareInboxCancellation = { at: number; bindings?: SquareInboxBinding[] };
|
|
4
4
|
export type SquareInboxState = { coordinate: SquareInboxCoordinate; bindings?: SquareInboxBinding[] };
|
|
@@ -3,38 +3,26 @@
|
|
|
3
3
|
"SessionStart": [
|
|
4
4
|
{ "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
|
|
5
5
|
],
|
|
6
|
-
"SessionResume": [
|
|
7
|
-
{ "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
|
|
8
|
-
],
|
|
9
6
|
"SessionEnd": [
|
|
10
7
|
{ "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 3, "statusMessage": "Square" }] }
|
|
11
8
|
],
|
|
12
9
|
"UserPromptSubmit": [
|
|
13
10
|
{ "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
|
|
14
11
|
],
|
|
12
|
+
"PreToolUse": [
|
|
13
|
+
{ "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
|
|
14
|
+
],
|
|
15
15
|
"PostToolUse": [
|
|
16
|
-
{
|
|
17
|
-
"hooks": [
|
|
18
|
-
{
|
|
19
|
-
"type": "command",
|
|
20
|
-
"command": "square codex-hook",
|
|
21
|
-
"timeout": 5,
|
|
22
|
-
"statusMessage": "Square"
|
|
23
|
-
}
|
|
24
|
-
]
|
|
25
|
-
}
|
|
16
|
+
{ "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
|
|
26
17
|
],
|
|
27
18
|
"Stop": [
|
|
28
|
-
{
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
]
|
|
37
|
-
}
|
|
19
|
+
{ "hooks": [
|
|
20
|
+
{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" },
|
|
21
|
+
{ "type": "command", "command": "square codex-watch", "timeout": 86400, "async": true, "statusMessage": "Square" }
|
|
22
|
+
] }
|
|
23
|
+
],
|
|
24
|
+
"Interrupt": [
|
|
25
|
+
{ "hooks": [{ "type": "command", "command": "square codex-interrupt", "timeout": 3, "statusMessage": "Square" }] }
|
|
38
26
|
]
|
|
39
27
|
}
|
|
40
28
|
}
|
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { renderDeliveryReminder } from '../../delivery-frame.js';
|
|
3
|
+
import type { DeliveryTransport } from '../../delivery-ledger.js';
|
|
2
4
|
import { canonicalPath } from '../../canonical-path.js';
|
|
3
|
-
import { connectNative, type OpenCodeNativeSession, type OpenCodeSessionCapability } from '../../packages/agent-delivery/src/opencode-native.js';
|
|
4
|
-
import { cancelOpenCodeDeliveries, endOpenCodeSession,
|
|
5
|
+
import { connectNative, createNativeInputId, sendNativeText, type OpenCodeNativeSession, type OpenCodeSessionCapability } from '../../packages/agent-delivery/src/opencode-native.js';
|
|
6
|
+
import { cancelOpenCodeDeliveries, currentOpenCodeGeneration, detachOpenCodeSession, endOpenCodeSession, listOpenCodeSessions, observeOpenCodeContext, observeOpenCodeSessions, publishOpenCodeAddress, receiveOpenCodePending, type OpenCodeContextMessage } from '../opencode-shared/delivery.js';
|
|
7
|
+
|
|
8
|
+
const routeKind = 'opencode-server' as const;
|
|
9
|
+
|
|
10
|
+
export function createOpenCodeWakeTransport(target: OpenCodeNativeSession, lifetime: AbortSignal): DeliveryTransport {
|
|
11
|
+
return {
|
|
12
|
+
kind: routeKind,
|
|
13
|
+
async send(card, address, context) {
|
|
14
|
+
const signal = AbortSignal.any([lifetime, context.signal]);
|
|
15
|
+
if (address.sessionId !== target.sessionId || signal.aborted) return 'not-capable';
|
|
16
|
+
const messageId = createNativeInputId();
|
|
17
|
+
const text = renderDeliveryReminder([card.text], { tagged: true });
|
|
18
|
+
const payloadHash = createHash('sha256').update(text).digest('hex');
|
|
19
|
+
if (!await context.prepare({ token: context.token, messageId, payloadHash }) || signal.aborted
|
|
20
|
+
|| !await context.beforeSend() || signal.aborted || Date.now() >= context.deadline) return 'not-capable';
|
|
21
|
+
const result = await sendNativeText(target, text, {
|
|
22
|
+
inputId: messageId, delivery: 'steer', timeoutMs: Math.max(1, context.deadline - Date.now()), signal,
|
|
23
|
+
});
|
|
24
|
+
if (result.state === 'accepted') return 'accepted';
|
|
25
|
+
if (result.state === 'unavailable') return 'failed';
|
|
26
|
+
return 'unknown';
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
5
30
|
|
|
6
31
|
interface OpenCodePluginContext {
|
|
7
32
|
readonly app: { readonly version: string };
|
|
@@ -12,17 +37,33 @@ interface OpenCodePluginContext {
|
|
|
12
37
|
};
|
|
13
38
|
readonly event: { subscribe(options: { signal: AbortSignal }): AsyncIterable<{ type: string; data: { sessionID?: string; reason?: string } }> };
|
|
14
39
|
}
|
|
15
|
-
interface Receiver { readonly abort: AbortController; readonly running: Promise<void>; settled: boolean }
|
|
40
|
+
interface Receiver { readonly abort: AbortController; readonly running: Promise<void>; readonly generation: string; settled: boolean }
|
|
16
41
|
|
|
17
|
-
/**
|
|
42
|
+
/** OpenCode v2 server plugin. Harness identity belongs to ctx, never process.env. */
|
|
18
43
|
export default {
|
|
19
44
|
id: 'square',
|
|
20
45
|
async setup(ctx: OpenCodePluginContext): Promise<() => Promise<void>> {
|
|
21
|
-
|
|
46
|
+
// OpenCode publishes semantic versions; unknown version forms are not evidence of a v2 host.
|
|
47
|
+
const version = ctx?.app?.version;
|
|
48
|
+
const reason = typeof version !== 'string' || !/^2\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)
|
|
49
|
+
? 'expected an OpenCode 2.x semantic version'
|
|
50
|
+
: typeof ctx.location?.directory !== 'string' || !ctx.location.directory ||
|
|
51
|
+
typeof ctx.location.project?.id !== 'string' || !ctx.location.project.id ||
|
|
52
|
+
(ctx.location.workspaceID !== undefined && typeof ctx.location.workspaceID !== 'string')
|
|
53
|
+
? 'missing location directory or project identity'
|
|
54
|
+
: typeof ctx.session?.hook !== 'function' || typeof ctx.session.get !== 'function' || typeof ctx.session.prompt !== 'function'
|
|
55
|
+
? 'missing session hook, get, or prompt capability'
|
|
56
|
+
: typeof ctx.event?.subscribe !== 'function' ? 'missing event subscription capability' : undefined;
|
|
57
|
+
if (reason) {
|
|
58
|
+
console.warn(`Square OpenCode plugin inactive: ${reason}.`);
|
|
59
|
+
return async () => {};
|
|
60
|
+
}
|
|
22
61
|
const env = { ...process.env };
|
|
23
|
-
const
|
|
62
|
+
const discovery = await observeOpenCodeSessions(env);
|
|
24
63
|
const lifetime = new AbortController();
|
|
25
64
|
const receivers = new Map<string, Receiver>(); // Operational handles only; presence owns membership.
|
|
65
|
+
const interrupting = new Set<string>();
|
|
66
|
+
const cancellationFailed = new Set<string>();
|
|
26
67
|
const directory = await canonicalPath(ctx.location.directory);
|
|
27
68
|
let coordinating = Promise.resolve();
|
|
28
69
|
const coordinate = (operation: () => Promise<void>) => {
|
|
@@ -49,31 +90,49 @@ export default {
|
|
|
49
90
|
return target;
|
|
50
91
|
};
|
|
51
92
|
const reconcile = async (activeSession?: string) => {
|
|
52
|
-
const candidates = new Set(await
|
|
93
|
+
const candidates = new Set(await listOpenCodeSessions(env));
|
|
53
94
|
if (activeSession) candidates.add(activeSession);
|
|
54
95
|
for (const sessionId of candidates) {
|
|
55
96
|
if (lifetime.signal.aborted) return;
|
|
97
|
+
if (interrupting.has(sessionId) || cancellationFailed.has(sessionId)) continue;
|
|
56
98
|
const target = await validate(sessionId);
|
|
57
|
-
if (!target) {
|
|
58
|
-
|
|
59
|
-
|
|
99
|
+
if (!target) {
|
|
100
|
+
const receiver = receivers.get(sessionId);
|
|
101
|
+
stop(sessionId);
|
|
102
|
+
if (receiver) await detachOpenCodeSession(sessionId, receiver.generation, env);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const existing = receivers.get(sessionId);
|
|
106
|
+
if (existing?.settled === false || interrupting.has(sessionId)) continue;
|
|
107
|
+
const generation = existing?.generation ?? await publishOpenCodeAddress(sessionId, env, lifetime.signal);
|
|
108
|
+
if (!generation) { stop(sessionId); continue; }
|
|
109
|
+
if (lifetime.signal.aborted || interrupting.has(sessionId)) { await detachOpenCodeSession(sessionId, generation, env); continue; }
|
|
60
110
|
const abort = new AbortController();
|
|
61
111
|
const signal = AbortSignal.any([lifetime.signal, abort.signal]);
|
|
62
|
-
const receiver: Receiver = { abort, settled: false, running: receiveOpenCodePending(target, env, signal).catch(() => {}).finally(() => { receiver.settled = true; }) };
|
|
112
|
+
const receiver: Receiver = { abort, generation, settled: false, running: receiveOpenCodePending(sessionId, generation, createOpenCodeWakeTransport(target, signal), env, signal).catch(() => {}).finally(() => { receiver.settled = true; }) };
|
|
63
113
|
receivers.set(sessionId, receiver);
|
|
64
114
|
}
|
|
65
115
|
for (const sessionId of receivers.keys()) if (!candidates.has(sessionId)) stop(sessionId);
|
|
66
116
|
};
|
|
67
117
|
const cancel = async (sessionId: string) => {
|
|
68
|
-
|
|
69
|
-
|
|
118
|
+
const receiver = receivers.get(sessionId);
|
|
119
|
+
if (!receiver) return;
|
|
70
120
|
stop(sessionId);
|
|
71
|
-
await
|
|
72
|
-
|
|
121
|
+
await receiver.running;
|
|
122
|
+
try {
|
|
123
|
+
if (!await cancelOpenCodeDeliveries(sessionId, receiver.generation, env)) throw new Error('OpenCode cancellation fence lost');
|
|
124
|
+
} catch (error) {
|
|
125
|
+
cancellationFailed.add(sessionId);
|
|
126
|
+
await detachOpenCodeSession(sessionId, receiver.generation, env).catch(() => {});
|
|
127
|
+
throw error;
|
|
128
|
+
} finally { interrupting.delete(sessionId); }
|
|
129
|
+
await reconcile(sessionId);
|
|
73
130
|
};
|
|
74
131
|
const end = async (sessionId: string) => {
|
|
132
|
+
const receiver = receivers.get(sessionId);
|
|
75
133
|
stop(sessionId);
|
|
76
|
-
await
|
|
134
|
+
const generation = receiver?.generation ?? await currentOpenCodeGeneration(sessionId, env);
|
|
135
|
+
if (generation) await endOpenCodeSession(sessionId, generation, env);
|
|
77
136
|
};
|
|
78
137
|
|
|
79
138
|
await ctx.session.hook('prompt', (event) => coordinate(() => reconcile(event.sessionID)));
|
|
@@ -81,14 +140,14 @@ export default {
|
|
|
81
140
|
await coordinate(() => reconcile(event.sessionID));
|
|
82
141
|
const receiver = receivers.get(event.sessionID);
|
|
83
142
|
if (!receiver || lifetime.signal.aborted) return;
|
|
84
|
-
await observeOpenCodeContext(event.sessionID, event.messages, env,
|
|
143
|
+
await observeOpenCodeContext(event.sessionID, receiver.generation, event.messages, env,
|
|
85
144
|
AbortSignal.any([lifetime.signal, receiver.abort.signal])).catch(() => {});
|
|
86
145
|
});
|
|
87
146
|
const presenceLoop = (async () => {
|
|
88
147
|
while (!lifetime.signal.aborted) {
|
|
89
|
-
const baseline =
|
|
148
|
+
const baseline = discovery.observer.generation;
|
|
90
149
|
await coordinate(() => reconcile());
|
|
91
|
-
await
|
|
150
|
+
await discovery.observer.hinted(baseline, 2000, lifetime.signal);
|
|
92
151
|
}
|
|
93
152
|
})().catch(() => {});
|
|
94
153
|
const eventLoop = (async () => {
|
|
@@ -96,8 +155,15 @@ export default {
|
|
|
96
155
|
const sessionId = event.data.sessionID;
|
|
97
156
|
if (!sessionId) continue;
|
|
98
157
|
if (event.type === 'session.deleted') await coordinate(() => end(sessionId));
|
|
99
|
-
else if (event.type === 'session.moved') await coordinate(async () => {
|
|
158
|
+
else if (event.type === 'session.moved') await coordinate(async () => {
|
|
159
|
+
const receiver = receivers.get(sessionId);
|
|
160
|
+
stop(sessionId);
|
|
161
|
+
if (receiver) await detachOpenCodeSession(sessionId, receiver.generation, env);
|
|
162
|
+
await reconcile();
|
|
163
|
+
});
|
|
100
164
|
else if (event.type === 'session.execution.interrupted' && event.data.reason === 'user') {
|
|
165
|
+
if (interrupting.has(sessionId) || !receivers.has(sessionId)) continue;
|
|
166
|
+
interrupting.add(sessionId);
|
|
101
167
|
receivers.get(sessionId)?.abort.abort();
|
|
102
168
|
await coordinate(() => cancel(sessionId));
|
|
103
169
|
}
|
|
@@ -106,11 +172,13 @@ export default {
|
|
|
106
172
|
// Do not await any native prompt in setup: plugin activation itself is lazy.
|
|
107
173
|
return async () => {
|
|
108
174
|
lifetime.abort();
|
|
109
|
-
|
|
110
|
-
const tasks = [presenceLoop, eventLoop, coordinating, ...
|
|
175
|
+
const captured = [...receivers.entries()];
|
|
176
|
+
const tasks = [presenceLoop, eventLoop, coordinating, ...captured.map(([id]) => stop(id))];
|
|
111
177
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
112
178
|
try { await Promise.race([Promise.allSettled(tasks), new Promise<void>((resolve) => { timer = setTimeout(resolve, 3000); })]); }
|
|
113
179
|
finally { if (timer !== undefined) clearTimeout(timer); }
|
|
180
|
+
await Promise.allSettled(captured.map(([id, receiver]) => detachOpenCodeSession(id, receiver.generation, env)));
|
|
181
|
+
await discovery.close();
|
|
114
182
|
// Instance unload is not session deletion: retain memberships and uncertain correlations.
|
|
115
183
|
};
|
|
116
184
|
},
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { DeliveryLedger, type DeliveryTransport } from '../../delivery-ledger.js';
|
|
3
|
+
import { retainDeliveryLedger } from '../../delivery-lifetime.js';
|
|
4
|
+
import { harnessOwner, decodeHarnessOwner } from '../../owner.js';
|
|
5
|
+
import { attachDeliverySessionRoutes, detachDeliverySession, markDeliverySessionDeleted } from '../../session-lifecycle.js';
|
|
6
|
+
|
|
7
|
+
const routeKind = 'opencode-server';
|
|
8
|
+
const ownerFor = (sessionId: string) => harnessOwner({ channel: 'opencode', session: sessionId });
|
|
9
|
+
const open = (env: NodeJS.ProcessEnv) => DeliveryLedger.open(env.SQUARE_DELIVERY ? { location: env.SQUARE_DELIVERY } : {});
|
|
10
|
+
const payloadHash = (payload: string) => createHash('sha256').update(payload).digest('hex');
|
|
11
|
+
export interface OpenCodeContextMessage { readonly id?: string; readonly role: string; readonly content: readonly { readonly type: string; readonly text?: string }[] }
|
|
12
|
+
|
|
13
|
+
export async function listOpenCodeSessions(env: NodeJS.ProcessEnv): Promise<readonly string[]> {
|
|
14
|
+
const ledger = await open(env);
|
|
15
|
+
try {
|
|
16
|
+
return (await ledger.sessions()).flatMap(session => {
|
|
17
|
+
const coordinate = decodeHarnessOwner(session.owner);
|
|
18
|
+
return session.state !== 'deleted' && coordinate?.channel === 'opencode' ? [coordinate.session] : [];
|
|
19
|
+
});
|
|
20
|
+
} finally { await ledger.close(); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The file observer is a hint; callers must always reread the ledger after an edge. */
|
|
24
|
+
export async function observeOpenCodeSessions(env: NodeJS.ProcessEnv) {
|
|
25
|
+
const ledger = await open(env);
|
|
26
|
+
const observer = await ledger.observe(ownerFor('discovery'));
|
|
27
|
+
return { observer, close: async () => { observer.close(); await ledger.close(); } };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function publishOpenCodeAddress(sessionId: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<string | undefined> {
|
|
31
|
+
const session = await attachDeliverySessionRoutes(ownerFor(sessionId), env, [{ kind: routeKind, address: { sessionId } }], signal);
|
|
32
|
+
return session ? session.generation : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function currentOpenCodeGeneration(sessionId: string, env: NodeJS.ProcessEnv): Promise<string | undefined> {
|
|
36
|
+
const ledger = await open(env);
|
|
37
|
+
try { return (await ledger.session(ownerFor(sessionId)))?.generation; } finally { await ledger.close(); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function detachOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<void> {
|
|
41
|
+
await detachDeliverySession(ownerFor(sessionId), generation, env);
|
|
42
|
+
}
|
|
43
|
+
export async function endOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<void> {
|
|
44
|
+
await markDeliverySessionDeleted(ownerFor(sessionId), generation, env);
|
|
45
|
+
}
|
|
46
|
+
export async function cancelOpenCodeDeliveries(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<boolean> {
|
|
47
|
+
const ledger = await open(env);
|
|
48
|
+
try { return await ledger.cancel({ owner: ownerFor(sessionId), generation }); } finally { await ledger.close(); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function textMessage(message: OpenCodeContextMessage): string | undefined {
|
|
52
|
+
if (message.role !== 'user' || message.id === undefined || !message.content.every(part => part.type === 'text' && typeof part.text === 'string')) return undefined;
|
|
53
|
+
return message.content.map(part => part.text!).join('');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Confirmation is observer-level: another host plugin may subsequently remove the message. */
|
|
57
|
+
export async function observeOpenCodeContext(sessionId: string, generation: string, messages: readonly OpenCodeContextMessage[], env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
|
|
58
|
+
const observed = new Map(messages.flatMap(message => {
|
|
59
|
+
const text = textMessage(message);
|
|
60
|
+
return text === undefined ? [] : [[message.id!, payloadHash(text)] as const];
|
|
61
|
+
}));
|
|
62
|
+
if (!observed.size || signal.aborted) return;
|
|
63
|
+
const ledger = await open(env);
|
|
64
|
+
try {
|
|
65
|
+
for (const attempt of await ledger.handoffs(ownerFor(sessionId), { generation })) {
|
|
66
|
+
if (signal.aborted) return;
|
|
67
|
+
const { correlation } = attempt;
|
|
68
|
+
if (attempt.routeKind === routeKind && correlation.token === attempt.token && correlation.messageId !== undefined
|
|
69
|
+
&& correlation.payloadHash !== undefined && observed.get(correlation.messageId) === correlation.payloadHash)
|
|
70
|
+
await ledger.observePresentation({ token: attempt.token, owner: ownerFor(sessionId), generation, routeKind, payloadHash: correlation.payloadHash }, { requireCurrent: true });
|
|
71
|
+
}
|
|
72
|
+
} finally { await ledger.close(); }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function receiveOpenCodePending(sessionId: string, generation: string, transport: DeliveryTransport, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
|
|
76
|
+
const owner = ownerFor(sessionId);
|
|
77
|
+
const ledger = await open(env);
|
|
78
|
+
const lifetime = retainDeliveryLedger(ledger);
|
|
79
|
+
const observer = await ledger.observe(owner);
|
|
80
|
+
try {
|
|
81
|
+
while (!signal.aborted) {
|
|
82
|
+
const baseline = observer.generation;
|
|
83
|
+
for (const card of await ledger.pending(owner, { generation })) {
|
|
84
|
+
if (signal.aborted) return;
|
|
85
|
+
lifetime.track(await ledger.dispatchHandoff(owner, card.key, transport, { generation, timeoutMs: 5000, signal }));
|
|
86
|
+
}
|
|
87
|
+
if (!signal.aborted) await observer.hinted(baseline, 2000, signal);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) { if (!signal.aborted) throw error; }
|
|
90
|
+
finally { observer.close(); await lifetime.close(); }
|
|
91
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface LegacyContext {
|
|
2
|
+
readonly serverUrl: string | URL;
|
|
3
|
+
readonly directory: string;
|
|
4
|
+
readonly project: { readonly id: string };
|
|
5
|
+
readonly client: { readonly session: {
|
|
6
|
+
get(input: unknown): Promise<{ data?: unknown; error?: unknown }>;
|
|
7
|
+
promptAsync(input: unknown): Promise<{ error?: unknown; response?: Response }>;
|
|
8
|
+
} };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Legacy loader: no storage imports until the actual host and runtime qualify. */
|
|
12
|
+
export default async function squareOpenCodeV1(ctx: LegacyContext) {
|
|
13
|
+
const shell = async (input: { sessionID?: string }, output: { env: Record<string, string> }) => {
|
|
14
|
+
if (typeof input.sessionID === 'string' && input.sessionID) output.env.OPENCODE_SESSION_ID = input.sessionID;
|
|
15
|
+
};
|
|
16
|
+
const reduced = { 'shell.env': shell };
|
|
17
|
+
try {
|
|
18
|
+
let response: Response;
|
|
19
|
+
try { response = await fetch(new URL('/global/health', ctx.serverUrl), { signal: AbortSignal.timeout(1500) }); }
|
|
20
|
+
catch { throw new Error('health endpoint unreachable (embedded TUI may not expose it)'); }
|
|
21
|
+
if (!response.ok) throw new Error(`health endpoint returned HTTP ${response.status} (embedded TUI may not expose it)`);
|
|
22
|
+
const health = await response.json() as { version?: unknown };
|
|
23
|
+
const match = typeof health.version === 'string' && /^(\d+)\.(\d+)\.(\d+)$/.exec(health.version);
|
|
24
|
+
if (!match || Number(match[1]) !== 1 || Number(match[2]) < 15 || (Number(match[2]) === 15 && Number(match[3]) < 11)) throw new Error('native version below resident floor or unknown');
|
|
25
|
+
if (typeof ctx.client?.session?.get !== 'function' || typeof ctx.client.session.promptAsync !== 'function') throw new Error('native session API unavailable');
|
|
26
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
27
|
+
const db = new DatabaseSync(':memory:');
|
|
28
|
+
try { db.exec('CREATE TABLE capability (value TEXT)'); db.prepare('INSERT INTO capability VALUES (?)').run('ok'); db.prepare('SELECT value FROM capability').get(); }
|
|
29
|
+
finally { db.close(); }
|
|
30
|
+
const { startReceiver } = await import('./runtime.js');
|
|
31
|
+
return { ...reduced, ...await startReceiver(ctx) };
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn(`[square] OpenCode 1.x reduced mode (no proactive delivery): ${error instanceof Error ? error.message : String(error)}`);
|
|
34
|
+
return reduced;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { canonicalPath } from '../../canonical-path.js';
|
|
2
|
+
import { cancelOpenCodeDeliveries, currentOpenCodeGeneration, detachOpenCodeSession, endOpenCodeSession, listOpenCodeSessions, observeOpenCodeContext, observeOpenCodeSessions, publishOpenCodeAddress, receiveOpenCodePending } from '../opencode-shared/delivery.js';
|
|
3
|
+
import { legacyTransport } from './transport.js';
|
|
4
|
+
|
|
5
|
+
interface Context {
|
|
6
|
+
readonly directory: string;
|
|
7
|
+
readonly project: { readonly id: string };
|
|
8
|
+
readonly client: { readonly session: {
|
|
9
|
+
get(input: unknown): Promise<{ data?: unknown; error?: unknown }>;
|
|
10
|
+
promptAsync(input: unknown): Promise<{ error?: unknown; response?: Response }>;
|
|
11
|
+
} };
|
|
12
|
+
}
|
|
13
|
+
interface NativeMessage { info?: { id?: string; sessionID?: string; role?: string }; parts?: { type?: string; text?: string; sessionID?: string; messageID?: string; synthetic?: boolean; ignored?: boolean }[] }
|
|
14
|
+
interface Receiver { abort: AbortController; running: Promise<void>; generation: string; settled: boolean }
|
|
15
|
+
|
|
16
|
+
export async function startReceiver(ctx: Context) {
|
|
17
|
+
const env = { ...process.env };
|
|
18
|
+
const directory = await canonicalPath(ctx.directory);
|
|
19
|
+
const discovery = await observeOpenCodeSessions(env);
|
|
20
|
+
const lifetime = new AbortController();
|
|
21
|
+
const receivers = new Map<string, Receiver>();
|
|
22
|
+
const terminal = new Set<string>();
|
|
23
|
+
const interrupting = new Set<string>();
|
|
24
|
+
const cancellationFailed = new Set<string>();
|
|
25
|
+
let coordinating = Promise.resolve();
|
|
26
|
+
const coordinate = (operation: () => Promise<void>) => {
|
|
27
|
+
coordinating = coordinating.then(() => lifetime.signal.aborted ? undefined : operation()).catch(() => {});
|
|
28
|
+
return coordinating;
|
|
29
|
+
};
|
|
30
|
+
const stop = (id: string) => {
|
|
31
|
+
const receiver = receivers.get(id);
|
|
32
|
+
receiver?.abort.abort();
|
|
33
|
+
receivers.delete(id);
|
|
34
|
+
return receiver?.running;
|
|
35
|
+
};
|
|
36
|
+
const validate = async (id: string) => {
|
|
37
|
+
const result = await ctx.client.session.get({ path: { id }, signal: lifetime.signal }).catch(() => undefined);
|
|
38
|
+
const info = result?.data as { id?: string; projectID?: string; directory?: string } | undefined;
|
|
39
|
+
return !result?.error && info?.id === id && info.projectID === ctx.project.id && typeof info.directory === 'string'
|
|
40
|
+
&& await canonicalPath(info.directory) === directory;
|
|
41
|
+
};
|
|
42
|
+
const reconcile = async (active?: string) => {
|
|
43
|
+
const candidates = new Set(await listOpenCodeSessions(env));
|
|
44
|
+
if (active) candidates.add(active);
|
|
45
|
+
for (const id of candidates) {
|
|
46
|
+
if (lifetime.signal.aborted) return;
|
|
47
|
+
if (interrupting.has(id) || cancellationFailed.has(id)) continue;
|
|
48
|
+
if (terminal.has(id) || !await validate(id) || terminal.has(id)) {
|
|
49
|
+
const receiver = receivers.get(id);
|
|
50
|
+
stop(id);
|
|
51
|
+
if (receiver) await detachOpenCodeSession(id, receiver.generation, env);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const existing = receivers.get(id);
|
|
55
|
+
if (existing?.settled === false || interrupting.has(id)) continue;
|
|
56
|
+
const generation = existing?.generation ?? await publishOpenCodeAddress(id, env, lifetime.signal);
|
|
57
|
+
if (!generation) { stop(id); continue; }
|
|
58
|
+
if (terminal.has(id) || interrupting.has(id) || lifetime.signal.aborted) { await detachOpenCodeSession(id, generation, env); continue; }
|
|
59
|
+
const abort = new AbortController();
|
|
60
|
+
const signal = AbortSignal.any([lifetime.signal, abort.signal]);
|
|
61
|
+
const receiver: Receiver = { abort, generation, settled: false, running: receiveOpenCodePending(id, generation, legacyTransport(ctx.client, id, signal), env, signal).catch(() => {}).finally(() => { receiver.settled = true; }) };
|
|
62
|
+
receivers.set(id, receiver);
|
|
63
|
+
}
|
|
64
|
+
for (const id of receivers.keys()) if (!candidates.has(id)) stop(id);
|
|
65
|
+
};
|
|
66
|
+
const loop = (async () => {
|
|
67
|
+
while (!lifetime.signal.aborted) {
|
|
68
|
+
const baseline = discovery.observer.generation;
|
|
69
|
+
await coordinate(() => reconcile());
|
|
70
|
+
await discovery.observer.hinted(baseline, 2000, lifetime.signal);
|
|
71
|
+
}
|
|
72
|
+
})().catch(() => {});
|
|
73
|
+
let disposal: Promise<void> | undefined;
|
|
74
|
+
return {
|
|
75
|
+
'experimental.chat.messages.transform': async (_input: unknown, output: { messages: NativeMessage[] }) => {
|
|
76
|
+
const ids = new Set(output.messages.map(message => message.info?.sessionID));
|
|
77
|
+
if (ids.size !== 1 || typeof [...ids][0] !== 'string') return;
|
|
78
|
+
const id = [...ids][0]!;
|
|
79
|
+
if (terminal.has(id)) return;
|
|
80
|
+
await coordinate(() => reconcile(id));
|
|
81
|
+
const receiver = receivers.get(id);
|
|
82
|
+
if (!receiver || terminal.has(id) || lifetime.signal.aborted) return;
|
|
83
|
+
const messages = output.messages.flatMap(({ info, parts }) => {
|
|
84
|
+
if (info?.role !== 'user' || !info.id || !parts?.length || !parts.every(part => part.type === 'text'
|
|
85
|
+
&& typeof part.text === 'string' && part.sessionID === id && part.messageID === info.id && !part.synthetic && !part.ignored)) return [];
|
|
86
|
+
return [{ id: info.id, role: 'user', content: parts.map(part => ({ type: 'text', text: part.text })) }];
|
|
87
|
+
});
|
|
88
|
+
await observeOpenCodeContext(id, receiver.generation, messages, env, AbortSignal.any([lifetime.signal, receiver.abort.signal])).catch(() => {});
|
|
89
|
+
},
|
|
90
|
+
event: async ({ event }: { event: { type: string; properties?: { sessionID?: string; error?: { name?: string }; info?: { id?: string; projectID?: string; directory?: string } } } }) => {
|
|
91
|
+
const interrupted = event.type === 'session.error' && event.properties?.error?.name === 'MessageAbortedError'
|
|
92
|
+
? event.properties.sessionID : undefined;
|
|
93
|
+
if (interrupted) {
|
|
94
|
+
const receiver = receivers.get(interrupted);
|
|
95
|
+
if (!receiver || interrupting.has(interrupted)) return;
|
|
96
|
+
interrupting.add(interrupted);
|
|
97
|
+
stop(interrupted);
|
|
98
|
+
await coordinate(async () => {
|
|
99
|
+
try {
|
|
100
|
+
await receiver.running;
|
|
101
|
+
if (!await cancelOpenCodeDeliveries(interrupted, receiver.generation, env)) throw new Error('OpenCode cancellation fence lost');
|
|
102
|
+
} catch (error) {
|
|
103
|
+
cancellationFailed.add(interrupted);
|
|
104
|
+
await detachOpenCodeSession(interrupted, receiver.generation, env).catch(() => {});
|
|
105
|
+
throw error;
|
|
106
|
+
} finally { interrupting.delete(interrupted); }
|
|
107
|
+
await reconcile(interrupted);
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const info = event.properties?.info;
|
|
112
|
+
if (event.type !== 'session.deleted' || !info?.id || info.projectID !== ctx.project.id || typeof info.directory !== 'string'
|
|
113
|
+
|| await canonicalPath(info.directory).catch(() => undefined) !== directory) return;
|
|
114
|
+
const id = info.id;
|
|
115
|
+
terminal.add(id);
|
|
116
|
+
const receiver = receivers.get(id);
|
|
117
|
+
stop(id);
|
|
118
|
+
await coordinate(async () => {
|
|
119
|
+
const generation = receiver?.generation ?? await currentOpenCodeGeneration(id, env);
|
|
120
|
+
if (generation) await endOpenCodeSession(id, generation, env);
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
dispose: () => disposal ??= (async () => {
|
|
124
|
+
lifetime.abort();
|
|
125
|
+
const captured = [...receivers.entries()];
|
|
126
|
+
const tasks = [loop, coordinating, ...captured.map(([id]) => stop(id))];
|
|
127
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
128
|
+
try { await Promise.race([Promise.allSettled(tasks), new Promise<void>(resolve => { timer = setTimeout(resolve, 3000); })]); }
|
|
129
|
+
finally { if (timer !== undefined) clearTimeout(timer); }
|
|
130
|
+
await Promise.allSettled(captured.map(([id, receiver]) => detachOpenCodeSession(id, receiver.generation, env)));
|
|
131
|
+
await discovery.close();
|
|
132
|
+
})(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { randomInt, createHash } from 'node:crypto';
|
|
2
|
+
import { renderDeliveryReminder } from '../../delivery-frame.js';
|
|
3
|
+
import type { DeliveryTransport } from '../../delivery-ledger.js';
|
|
4
|
+
|
|
5
|
+
type Client = { session: { promptAsync(input: unknown): Promise<{ error?: unknown; response?: Response }> } };
|
|
6
|
+
let lastTime = 0, counter = 0;
|
|
7
|
+
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
8
|
+
|
|
9
|
+
/** OpenCode's 48-bit timestamp/counter prefix followed by 14 random base62 digits. */
|
|
10
|
+
export function nativeMessageId(): string {
|
|
11
|
+
const now = Math.max(Date.now(), lastTime);
|
|
12
|
+
counter = now === lastTime ? counter + 1 : 0;
|
|
13
|
+
if (counter >= 0x1000) { lastTime = now + 1; counter = 0; } else lastTime = now;
|
|
14
|
+
const prefix = (BigInt(lastTime) * 0x1000n + BigInt(counter)).toString(16).slice(-12).padStart(12, '0');
|
|
15
|
+
return `msg_${prefix}${Array.from({ length: 14 }, () => alphabet[randomInt(62)]).join('')}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function legacyTransport(client: Client, sessionId: string, lifetime: AbortSignal): DeliveryTransport {
|
|
19
|
+
return {
|
|
20
|
+
kind: 'opencode-server',
|
|
21
|
+
async send(card, address, context) {
|
|
22
|
+
const signal = AbortSignal.any([lifetime, context.signal]);
|
|
23
|
+
if (address.sessionId !== sessionId || signal.aborted) return 'not-capable';
|
|
24
|
+
const messageId = nativeMessageId();
|
|
25
|
+
const text = renderDeliveryReminder([card.text], { tagged: true });
|
|
26
|
+
const payloadHash = createHash('sha256').update(text).digest('hex');
|
|
27
|
+
if (!await context.prepare({ token: context.token, messageId, payloadHash }) || signal.aborted
|
|
28
|
+
|| !await context.beforeSend() || signal.aborted || Date.now() >= context.deadline) return 'not-capable';
|
|
29
|
+
let onAbort: (() => void) | undefined;
|
|
30
|
+
try {
|
|
31
|
+
await Promise.race([
|
|
32
|
+
client.session.promptAsync({ path: { id: sessionId }, body: { messageID: messageId, parts: [{ type: 'text', text }] }, signal }),
|
|
33
|
+
new Promise<never>((_, reject) => { onAbort = () => reject(signal.reason); signal.addEventListener('abort', onAbort, { once: true }); if (signal.aborted) onAbort(); }),
|
|
34
|
+
]);
|
|
35
|
+
// promptAsync acknowledges only submission, not native acceptance or presentation.
|
|
36
|
+
return 'unknown';
|
|
37
|
+
} catch { return 'unknown'; }
|
|
38
|
+
finally { if (onAbort) signal.removeEventListener('abort', onAbort); }
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|