@astrosheep/square 0.3.63 → 0.3.65
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/claude-plugin/.claude-plugin/plugin.json +3 -2
- package/claude-plugin/hooks/hooks.json +1 -22
- package/claude-plugin/hooks/register.js +119 -0
- package/claude-plugin/types.d.ts +7 -0
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity-feed.d.ts +1 -10
- package/dist/activity-feed.js +1 -27
- package/dist/activity.js +57 -25
- package/dist/artifact.d.ts +1 -1
- package/dist/artifact.js +24 -25
- package/dist/attention-presentation.js +1 -0
- package/dist/automatic-session.d.ts +3 -1
- package/dist/automatic-session.js +21 -34
- package/dist/boundary-presentation.js +4 -13
- package/dist/canonical-path.d.ts +11 -0
- package/dist/canonical-path.js +47 -0
- package/dist/catch-decisions.js +14 -10
- package/dist/catch-selection.d.ts +6 -0
- package/dist/catch-selection.js +14 -0
- package/dist/claude-delivery.d.ts +11 -0
- package/dist/claude-delivery.js +145 -0
- package/dist/claude-mod.d.ts +8 -0
- package/dist/claude-mod.js +164 -0
- package/dist/cli/context.d.ts +0 -1
- package/dist/cli/context.js +9 -21
- package/dist/cli/harness-command.d.ts +0 -1
- package/dist/cli/harness-command.js +0 -6
- package/dist/cli/observation-commands.d.ts +1 -1
- package/dist/cli/observation-commands.js +31 -58
- package/dist/cli/program.js +20 -18
- package/dist/cli/registry.js +2 -2
- package/dist/cli/square-commands.js +10 -6
- package/dist/codex-boundary-state.d.ts +2 -2
- package/dist/codex-boundary-state.js +4 -4
- package/dist/codex-hook.js +12 -15
- package/dist/codex-queue.d.ts +2 -2
- package/dist/codex-queue.js +7 -5
- package/dist/decisions.d.ts +2 -6
- package/dist/decisions.js +16 -17
- package/dist/delivery-health.d.ts +1 -2
- package/dist/delivery-health.js +4 -5
- package/dist/delivery-operations.d.ts +2 -5
- package/dist/delivery-operations.js +77 -214
- package/dist/delivery.d.ts +25 -47
- package/dist/delivery.js +14 -46
- package/dist/diagnostic-redaction.js +7 -9
- package/dist/file-changes.d.ts +0 -1
- package/dist/file-changes.js +2 -19
- package/dist/harness-claude.js +8 -6
- package/dist/harness-codex.d.ts +0 -2
- package/dist/harness-codex.js +0 -2
- package/dist/harness-links.js +13 -17
- package/dist/harness.js +3 -7
- package/dist/help.js +4 -3
- package/dist/host-ledger-file-adapter.d.ts +17 -8
- package/dist/host-ledger-file-adapter.js +164 -80
- package/dist/host-ledger.d.ts +55 -50
- package/dist/inbox.d.ts +0 -3
- package/dist/inbox.js +88 -50
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/list.js +1 -4
- package/dist/mcp.js +24 -41
- package/dist/mention-parse.js +3 -9
- package/dist/model.d.ts +11 -2
- package/dist/model.js +1 -1
- package/dist/notifications.d.ts +3 -16
- package/dist/notifications.js +41 -122
- package/dist/opencode-delivery.d.ts +14 -0
- package/dist/opencode-delivery.js +171 -0
- package/dist/opencode.d.ts +39 -18
- package/dist/opencode.js +145 -40
- package/dist/packages/agent-delivery/src/claude-auth.d.ts +13 -0
- package/dist/packages/agent-delivery/src/claude-auth.js +75 -0
- package/dist/packages/agent-delivery/src/claude-native.d.ts +28 -0
- package/dist/packages/agent-delivery/src/claude-native.js +68 -0
- package/dist/packages/agent-delivery/src/claude.d.ts +14 -0
- package/dist/packages/agent-delivery/src/claude.js +66 -0
- package/dist/packages/agent-delivery/src/deadline.d.ts +11 -0
- package/dist/packages/agent-delivery/src/deadline.js +41 -0
- package/dist/packages/agent-delivery/src/errors.d.ts +16 -0
- package/dist/packages/agent-delivery/src/errors.js +41 -0
- package/dist/packages/agent-delivery/src/guards.d.ts +6 -0
- package/dist/packages/agent-delivery/src/guards.js +6 -0
- package/dist/packages/agent-delivery/src/index.d.ts +83 -0
- package/dist/packages/agent-delivery/src/index.js +110 -0
- package/dist/packages/agent-delivery/src/local-endpoint.d.ts +26 -0
- package/dist/packages/agent-delivery/src/local-endpoint.js +114 -0
- package/dist/packages/agent-delivery/src/opencode-native.d.ts +33 -0
- package/dist/packages/agent-delivery/src/opencode-native.js +75 -0
- package/dist/packages/agent-delivery/src/opencode.d.ts +14 -0
- package/dist/packages/agent-delivery/src/opencode.js +127 -0
- package/dist/packages/agent-delivery/src/paseo-native.d.ts +58 -0
- package/dist/packages/agent-delivery/src/paseo-native.js +122 -0
- package/dist/packages/agent-delivery/src/paseo.d.ts +19 -0
- package/dist/packages/agent-delivery/src/paseo.js +106 -0
- package/dist/packages/agent-delivery/src/pi-client.d.ts +10 -0
- package/dist/packages/agent-delivery/src/pi-client.js +106 -0
- package/dist/packages/agent-delivery/src/pi-protocol.d.ts +15 -0
- package/dist/packages/agent-delivery/src/pi-protocol.js +50 -0
- package/dist/packages/agent-delivery/src/pi-send.d.ts +23 -0
- package/dist/packages/agent-delivery/src/pi-send.js +4 -0
- package/dist/packages/agent-delivery/src/pi.d.ts +25 -0
- package/dist/packages/agent-delivery/src/pi.js +209 -0
- package/dist/packages/agent-delivery/src/types.d.ts +120 -0
- package/dist/participant-host.js +12 -13
- package/dist/participant-identity.d.ts +21 -0
- package/dist/participant-identity.js +32 -11
- package/dist/paseo-delivery.d.ts +2 -2
- package/dist/paseo-delivery.js +35 -27
- package/dist/paseo-timeline.js +25 -21
- package/dist/paseo.d.ts +2 -2
- package/dist/paseo.js +1 -2
- package/dist/ports.d.ts +7 -20
- package/dist/presentation-operations.js +16 -4
- package/dist/presentation.d.ts +19 -10
- package/dist/presentation.js +99 -177
- package/dist/registry.d.ts +15 -66
- package/dist/registry.js +45 -123
- package/dist/routes.d.ts +1 -28
- package/dist/routes.js +22 -104
- package/dist/runtime.d.ts +3 -9
- package/dist/runtime.js +14 -67
- package/dist/search.d.ts +0 -2
- package/dist/search.js +3 -10
- package/dist/square-actions.js +48 -72
- package/dist/square-application.js +32 -30
- package/dist/square-core.d.ts +3 -0
- package/dist/square-core.js +22 -5
- package/dist/square-facade.d.ts +2 -0
- package/dist/square-file-adapter.d.ts +2 -1
- package/dist/square-file-adapter.js +6 -9
- package/dist/square-projections.d.ts +40 -45
- package/dist/square-projections.js +60 -74
- package/dist/square-state.js +1 -4
- package/dist/square-storage.d.ts +5 -8
- package/dist/square-storage.js +13 -36
- package/dist/square-wiring.d.ts +2 -7
- package/dist/square-wiring.js +6 -24
- package/dist/stream.js +4 -6
- package/dist/time.js +2 -7
- package/dist/views.d.ts +6 -18
- package/dist/views.js +14 -21
- package/dist/wake-attempts.d.ts +2 -56
- package/dist/wake-attempts.js +15 -110
- package/dist/wake-eligibility.d.ts +43 -0
- package/dist/wake-eligibility.js +106 -0
- package/dist/wake-evidence.d.ts +32 -9
- package/dist/wake-evidence.js +29 -24
- package/dist/wake-sink.d.ts +13 -9
- package/dist/wake-sink.js +35 -54
- package/dist/watch.js +13 -21
- package/extensions/square-pi.js +27 -13
- package/guides/change-waits.md +1 -1
- package/guides/claude-native.md +101 -0
- package/guides/host-ledger.md +3 -1
- package/guides/participant.md +14 -7
- package/guides/pi-native-delivery.md +49 -0
- package/package.json +4 -3
- package/dist/claude-hook.d.ts +0 -10
- package/dist/claude-hook.js +0 -60
- package/dist/paseo-connection.d.ts +0 -15
- package/dist/paseo-connection.js +0 -141
- package/dist/presence.d.ts +0 -2
- package/dist/presence.js +0 -24
- package/dist/presented.d.ts +0 -2
- package/dist/presented.js +0 -10
- package/dist/state-cell.d.ts +0 -14
- package/dist/wake-port.d.ts +0 -23
- package/dist/wake-port.js +0 -27
- /package/dist/{state-cell.js → packages/agent-delivery/src/types.js} +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
|
-
import fsSync from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { randomUUID } from 'node:crypto';
|
|
5
4
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
6
5
|
import { withFileLock as acquireFileLock } from './file-lock.js';
|
|
7
6
|
import { nameKey } from './model.js';
|
|
7
|
+
import { formatActivityId } from './square-core.js';
|
|
8
8
|
import { hostLedgerRoot } from './host-ledger-root.js';
|
|
9
9
|
import { observeFileMetadata } from './file-changes.js';
|
|
10
|
+
import { canonicalPath, canonicalPathSync } from './canonical-path.js';
|
|
10
11
|
const LOCK = { retryMs: 10 };
|
|
11
12
|
const RETENTION = 7 * 86400000;
|
|
12
13
|
// Prepare before locking: lock databases and their journals are local runtime data too.
|
|
@@ -24,32 +25,12 @@ async function withFileLock(file, options, fn) {
|
|
|
24
25
|
}
|
|
25
26
|
return acquireFileLock(file, options, fn);
|
|
26
27
|
}
|
|
27
|
-
async function canon(value) { const absolute = path.resolve(value); try {
|
|
28
|
-
return await fs.realpath(absolute);
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return absolute;
|
|
32
|
-
} }
|
|
33
|
-
function canonRoot(value) { let current = path.resolve(value), suffix = []; for (;;) {
|
|
34
|
-
try {
|
|
35
|
-
return path.join(fsSync.realpathSync.native(current), ...suffix.reverse());
|
|
36
|
-
}
|
|
37
|
-
catch (error) {
|
|
38
|
-
if (error.code !== 'ENOENT')
|
|
39
|
-
return path.resolve(value);
|
|
40
|
-
const parent = path.dirname(current);
|
|
41
|
-
if (parent === current)
|
|
42
|
-
return path.resolve(value);
|
|
43
|
-
suffix.push(path.basename(current));
|
|
44
|
-
current = parent;
|
|
45
|
-
}
|
|
46
|
-
} }
|
|
47
28
|
async function read(file, now, includeFuture = false) { try {
|
|
48
29
|
return (await fs.readFile(file, 'utf8')).split('\n').flatMap(line => { try {
|
|
49
|
-
const
|
|
30
|
+
const parsed = JSON.parse(line);
|
|
31
|
+
const row = decodeWakeRecord(parsed);
|
|
50
32
|
const at = row.at ?? row.updatedAt;
|
|
51
|
-
|
|
52
|
-
return row.v === 1 && typeof at === 'number' && (durableTerminal || at >= now - RETENTION) && (includeFuture || at <= now) ? [row] : [];
|
|
33
|
+
return row.v === 1 && typeof at === 'number' && (durableWake(row) || at >= now - RETENTION) && (includeFuture || at <= now) ? [row] : [];
|
|
53
34
|
}
|
|
54
35
|
catch {
|
|
55
36
|
return [];
|
|
@@ -89,21 +70,20 @@ async function write(file, rows, signal) {
|
|
|
89
70
|
}
|
|
90
71
|
}
|
|
91
72
|
function k(v) { return JSON.stringify([v.location, nameKey(v.participant), v.session, v.channel, v.activity, v.kind]); }
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
73
|
+
function durableWake(record) {
|
|
74
|
+
const row = record;
|
|
75
|
+
return row.kind === 'wake' && (row.outcome === 'accepted' || row.outcome === 'unknown' || row.outcome === 'dispatching');
|
|
76
|
+
}
|
|
77
|
+
function decodeWakeRecord(row) {
|
|
78
|
+
return row.kind === 'wake' && row.outcome === 'unknown' && row.unknownSource === undefined
|
|
79
|
+
? { ...row, unknownSource: row.signature === 'worker_interrupted_during_dispatch' ? 'interrupted' : 'transport' } : row;
|
|
80
|
+
}
|
|
81
|
+
/** Keep identity and native receipt, never a completed attempt's lease. */
|
|
82
|
+
function finishWake(current, outcome, details = {}) {
|
|
83
|
+
const { expiresAt: _expiresAt, ownerPid: _ownerPid, unknownSource: _unknownSource, ...rest } = current;
|
|
84
|
+
return { ...rest, ...details, kind: 'wake', outcome,
|
|
85
|
+
...(outcome === 'unknown' ? { unknownSource: details.unknownSource ?? 'transport' } : {}) };
|
|
101
86
|
}
|
|
102
|
-
catch (e) {
|
|
103
|
-
if (e.code === 'ENOENT')
|
|
104
|
-
return [];
|
|
105
|
-
throw e;
|
|
106
|
-
} }
|
|
107
87
|
function processAlive(pid) { if (pid === undefined)
|
|
108
88
|
return undefined; try {
|
|
109
89
|
process.kill(pid, 0);
|
|
@@ -112,14 +92,11 @@ function processAlive(pid) { if (pid === undefined)
|
|
|
112
92
|
catch (error) {
|
|
113
93
|
return error.code === 'ESRCH' ? false : true;
|
|
114
94
|
} }
|
|
115
|
-
function writeClaims(file, rows) { return write(file, rows); }
|
|
116
95
|
export class FileHostLedgerPort {
|
|
117
96
|
root;
|
|
118
|
-
claims;
|
|
119
97
|
clock;
|
|
120
98
|
constructor(o = {}) {
|
|
121
|
-
this.root =
|
|
122
|
-
this.claims = canonRoot(o.claimsPath ?? path.join(this.root, 'wake-claims.ndjsonl'));
|
|
99
|
+
this.root = canonicalPathSync(o.rootPath ?? hostLedgerRoot());
|
|
123
100
|
this.clock = o.now ?? Date.now;
|
|
124
101
|
}
|
|
125
102
|
file(kind) { return path.join(this.root, `${kind}.ndjsonl`); }
|
|
@@ -145,7 +122,7 @@ export class FileHostLedgerPort {
|
|
|
145
122
|
if (typeof at === 'number') {
|
|
146
123
|
if (at > now)
|
|
147
124
|
next = Math.min(next, at);
|
|
148
|
-
if (at + RETENTION + 1 > now && !(
|
|
125
|
+
if (at + RETENTION + 1 > now && !durableWake(row))
|
|
149
126
|
next = Math.min(next, at + RETENTION + 1);
|
|
150
127
|
}
|
|
151
128
|
if ('expiresAt' in row && typeof row.expiresAt === 'number' && row.expiresAt > now)
|
|
@@ -153,11 +130,15 @@ export class FileHostLedgerPort {
|
|
|
153
130
|
}
|
|
154
131
|
return next;
|
|
155
132
|
}
|
|
133
|
+
/** The ownership claim critical section: the same lock as claimPresence and finalize. */
|
|
134
|
+
withClaimLock(fn, signal) {
|
|
135
|
+
return withFileLock(path.join(this.root, 'presence-claim.lock'), { ...LOCK, signal }, fn);
|
|
136
|
+
}
|
|
156
137
|
async claimPresence(i, signal) {
|
|
157
|
-
const location = await
|
|
138
|
+
const location = await canonicalPath(i.location);
|
|
158
139
|
const record = { ...i, location, updatedAt: i.updatedAt ?? this.clock(), v: 1 };
|
|
159
140
|
try {
|
|
160
|
-
return await
|
|
141
|
+
return await this.withClaimLock(async () => {
|
|
161
142
|
const file = this.file('presence');
|
|
162
143
|
// The authoritative read, ownership check, and write share the presence file lock:
|
|
163
144
|
// a concurrent ensurePresence/removePresence between them must never be overwritten
|
|
@@ -170,28 +151,47 @@ export class FileHostLedgerPort {
|
|
|
170
151
|
await write(file, [...rows, record], signal);
|
|
171
152
|
return { status: 'acquired', record };
|
|
172
153
|
});
|
|
173
|
-
});
|
|
154
|
+
}, signal);
|
|
174
155
|
}
|
|
175
156
|
catch (error) {
|
|
176
157
|
return { status: 'degraded', record, error };
|
|
177
158
|
}
|
|
178
159
|
}
|
|
179
160
|
async ensurePresence(i) {
|
|
180
|
-
const record = { ...i, location: await
|
|
161
|
+
const record = { ...i, location: await canonicalPath(i.location), updatedAt: i.updatedAt ?? this.clock(), v: 1 };
|
|
181
162
|
try {
|
|
182
|
-
await withFileLock(this.file('presence') + '.lock', LOCK, async () =>
|
|
163
|
+
await withFileLock(this.file('presence') + '.lock', LOCK, async () => {
|
|
164
|
+
const rows = await read(this.file('presence'), this.clock(), true);
|
|
165
|
+
const current = rows.findLast((row) => k(row) === k(record));
|
|
166
|
+
const sameEpoch = current?.epoch === record.epoch;
|
|
167
|
+
const cancelledThrough = sameEpoch ? Math.max(current?.cancelledThrough ?? -1, record.cancelledThrough ?? -1) : record.cancelledThrough ?? -1;
|
|
168
|
+
await write(this.file('presence'), [...rows.filter((row) => k(row) !== k(record)), { ...record, ...(cancelledThrough < 0 ? {} : { cancelledThrough }) }]);
|
|
169
|
+
});
|
|
183
170
|
return { status: 'ensured', record };
|
|
184
171
|
}
|
|
185
172
|
catch (error) {
|
|
186
173
|
return { status: 'degraded', record, error };
|
|
187
174
|
}
|
|
188
175
|
}
|
|
176
|
+
/** Local batch suppression is monotonic only for the captured host generation. */
|
|
177
|
+
async suppressPresence(i, through) {
|
|
178
|
+
const record = { ...i, location: await canonicalPath(i.location) };
|
|
179
|
+
const file = this.file('presence');
|
|
180
|
+
return withFileLock(file + '.lock', LOCK, async () => {
|
|
181
|
+
const rows = await read(file, this.clock(), true);
|
|
182
|
+
const current = rows.findLast((row) => k(row) === k(record) && row.epoch === record.epoch);
|
|
183
|
+
if (!current)
|
|
184
|
+
return false;
|
|
185
|
+
await write(file, rows.map((row) => row === current ? { ...row, cancelledThrough: Math.max(row.cancelledThrough ?? -1, through) } : row));
|
|
186
|
+
return true;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
189
|
async removePresence(i) {
|
|
190
|
-
const record = { ...i, location: await
|
|
190
|
+
const record = { ...i, location: await canonicalPath(i.location) };
|
|
191
191
|
await withFileLock(this.file('presence') + '.lock', LOCK, async () => write(this.file('presence'), (await read(this.file('presence'), this.clock(), true)).filter((row) => k(row) !== k(record))));
|
|
192
192
|
}
|
|
193
193
|
async removePresenceIfUnchanged(i) {
|
|
194
|
-
const record = { ...i, location: await
|
|
194
|
+
const record = { ...i, location: await canonicalPath(i.location) };
|
|
195
195
|
return withFileLock(this.file('presence') + '.lock', LOCK, async () => {
|
|
196
196
|
const file = this.file('presence');
|
|
197
197
|
const rows = await read(file, this.clock(), true);
|
|
@@ -206,43 +206,127 @@ export class FileHostLedgerPort {
|
|
|
206
206
|
});
|
|
207
207
|
}
|
|
208
208
|
async listPresence(i = {}) {
|
|
209
|
-
const location = i.location === undefined ? undefined : await
|
|
209
|
+
const location = i.location === undefined ? undefined : await canonicalPath(i.location);
|
|
210
210
|
return (await read(this.file('presence'), i.now ?? this.clock())).filter((row) => (!location || row.location === location) && (!i.participant || nameKey(row.participant) === nameKey(i.participant)) && (!i.session || row.session === i.session)).map(({ scope: _scope, ...row }) => row);
|
|
211
211
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
212
|
+
/** One attempt row owns attention-wide exclusion and session-scoped history for a wake. */
|
|
213
|
+
async claimWakeAttempt(i) {
|
|
214
|
+
const location = await canonicalPath(i.attention.squarePath);
|
|
215
|
+
const activity = formatActivityId(i.attention.actIndex);
|
|
216
|
+
const at = i.now ?? this.clock();
|
|
217
|
+
const file = this.file('evidence');
|
|
218
|
+
try {
|
|
219
|
+
return await withFileLock(file + '.lock', LOCK, async () => {
|
|
220
|
+
const all = await read(file, at, true);
|
|
221
|
+
const attention = all.filter((row) => row.kind === 'wake' && row.location === location && nameKey(row.participant) === nameKey(i.attention.recipient) && row.activity === activity);
|
|
222
|
+
const terminal = attention.findLast((row) => row.outcome === 'accepted') ?? attention.findLast((row) => row.outcome === 'unknown');
|
|
223
|
+
if (terminal)
|
|
224
|
+
return { status: 'terminal', record: terminal };
|
|
225
|
+
const interrupted = attention.filter((row) => row.outcome === 'dispatching');
|
|
226
|
+
const busy = interrupted.find((row) => (row.expiresAt ?? 0) > at && processAlive(row.ownerPid) !== false);
|
|
227
|
+
if (busy)
|
|
228
|
+
return { status: 'busy', record: busy };
|
|
229
|
+
const nextNumber = (session) => attention.filter((row) => row.session === session).reduce((highest, row) => Math.max(highest, row.attemptN ?? 0), 0) + 1;
|
|
230
|
+
// An abandoned attempt may already have been sent, so it blocks every route.
|
|
231
|
+
if (interrupted.length) {
|
|
232
|
+
const recovered = interrupted.map((row) => ({ ...finishWake(row, 'unknown', {
|
|
233
|
+
unknownSource: 'interrupted', signature: 'worker_interrupted_during_dispatch',
|
|
234
|
+
message: 'The notification worker ended after dispatch began; transport acceptance is unknown.',
|
|
235
|
+
}), routeKind: row.routeKind ?? i.routeKind, attemptN: row.attemptN ?? nextNumber(row.session), v: 1 }));
|
|
236
|
+
const replacements = new Map(interrupted.map((row, index) => [row, recovered[index]]));
|
|
237
|
+
await write(file, all.map((row) => replacements.get(row) ?? row));
|
|
238
|
+
return { status: 'terminal', record: recovered[0] };
|
|
239
|
+
}
|
|
240
|
+
const attemptN = nextNumber(i.session), claimToken = `${process.pid}-${at}-${randomUUID()}`;
|
|
241
|
+
await write(file, [...all, { v: 1, at, location, participant: i.attention.recipient, session: i.session, activity,
|
|
242
|
+
kind: 'wake', outcome: 'dispatching', routeKind: i.routeKind, attemptN, claimToken, ownerPid: process.pid, expiresAt: at + i.leaseMs }]);
|
|
243
|
+
return { status: 'acquired', claimToken, attemptN };
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
return { status: 'degraded', error };
|
|
248
|
+
}
|
|
223
249
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
/** One locked, token-fenced replacement; each operation owns its state condition. */
|
|
251
|
+
async updateWake(input, update, at = this.clock()) {
|
|
252
|
+
if (!input.claimToken)
|
|
253
|
+
return false;
|
|
254
|
+
const location = await canonicalPath(input.location), file = this.file('evidence');
|
|
255
|
+
return withFileLock(file + '.lock', LOCK, async () => {
|
|
256
|
+
const all = await read(file, at, true);
|
|
257
|
+
const current = all.findLast((row) => row.kind === 'wake' && row.location === location && nameKey(row.participant) === nameKey(input.participant) && row.activity === input.activity && row.session === input.session && row.claimToken === input.claimToken);
|
|
258
|
+
const next = current && update(current);
|
|
259
|
+
if (!next)
|
|
260
|
+
return false;
|
|
261
|
+
if (next !== current)
|
|
262
|
+
await write(file, all.map((row) => row === current ? { ...next, v: 1 } : row));
|
|
263
|
+
return true;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async listWakeAttempts(i = {}) {
|
|
267
|
+
const attention = i.attention;
|
|
268
|
+
return this.listEvidence({ kind: 'wake', ...(attention === undefined ? {} : { location: await canonicalPath(attention.squarePath), participant: attention.recipient, activity: formatActivityId(attention.actIndex) }), session: i.session, now: i.now });
|
|
269
|
+
}
|
|
270
|
+
async prepareNativeWake(i) {
|
|
271
|
+
return this.updateWake(i, (current) => current.outcome === 'dispatching'
|
|
272
|
+
? { ...current, nativeDelivery: i.nativeDelivery, routeKind: i.routeKind, attemptN: i.attemptN } : undefined);
|
|
273
|
+
}
|
|
274
|
+
async confirmWakeAdmission(i) {
|
|
275
|
+
return this.updateWake(i, (current) => {
|
|
276
|
+
if (!current.nativeDelivery || !['dispatching', 'unknown', 'accepted'].includes(current.outcome))
|
|
277
|
+
return undefined;
|
|
278
|
+
return current.outcome === 'accepted' ? current : finishWake(current, 'accepted', { signature: 'native_queue_confirmed', message: undefined, diagnostic: undefined });
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
async claimEvidence(i) { if (i.kind === 'wake')
|
|
282
|
+
return { status: 'degraded', error: new Error('wake attempts are claimed with claimWakeAttempt') }; const f = this.file('evidence'), at = i.now ?? this.clock(), { leaseMs, ...claim } = i, r = { ...claim, location: await canonicalPath(i.location), outcome: 'dispatching', at, expiresAt: at + leaseMs, v: 1 }; try {
|
|
233
283
|
return await withFileLock(f + '.lock', LOCK, async () => { const all = await read(f, at, true), matching = all.filter(x => x.kind === r.kind && x.location === r.location && nameKey(x.participant) === nameKey(r.participant) && x.activity === r.activity && x.session === r.session), old = matching.findLast(x => x.outcome === 'dispatching'); if (old !== undefined && typeof old.expiresAt === 'number' && old.expiresAt > at)
|
|
234
|
-
return { status: 'busy', record: old };
|
|
235
|
-
return { status: 'delivered', record: matching.findLast(x => x.outcome === '
|
|
284
|
+
return { status: 'busy', record: old }; if (r.kind === 'presentation' && matching.some(x => x.outcome === 'presented'))
|
|
285
|
+
return { status: 'delivered', record: matching.findLast(x => x.outcome === 'presented') }; const claimToken = `${process.pid}-${at}-${Math.random().toString(36).slice(2)}`, row = { ...r, claimToken }; await write(f, [...all.filter(x => !(x.kind === r.kind && x.location === r.location && nameKey(x.participant) === nameKey(r.participant) && x.activity === r.activity && x.session === r.session && (x.outcome === 'dispatching' || x.outcome === 'released'))), row]); return { status: 'acquired', claimToken }; });
|
|
236
286
|
}
|
|
237
287
|
catch (error) {
|
|
238
288
|
return { status: 'degraded', error };
|
|
239
289
|
} }
|
|
240
|
-
async releaseEvidence(i) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
290
|
+
async releaseEvidence(i) {
|
|
291
|
+
if (i.kind === 'wake') {
|
|
292
|
+
const { signature, message, diagnostic, routeKind, attemptN } = i;
|
|
293
|
+
const details = Object.fromEntries(Object.entries({ signature, message, diagnostic, routeKind, attemptN }).filter(([, value]) => value !== undefined));
|
|
294
|
+
await this.updateWake(i, (current) => current.outcome === 'dispatching'
|
|
295
|
+
? finishWake(current, 'released', { ...details, at: i.now ?? this.clock() }) : undefined, i.now);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const f = this.file('evidence'), location = await canonicalPath(i.location), at = i.now ?? this.clock();
|
|
299
|
+
await withFileLock(f + '.lock', LOCK, async () => { const all = await read(f, at, true), same = (x) => x.kind === i.kind && x.location === location && nameKey(x.participant) === nameKey(i.participant) && x.activity === i.activity && x.session === i.session, current = all.findLast(x => same(x) && x.outcome === 'dispatching'); if (current === undefined || current.claimToken !== i.claimToken)
|
|
300
|
+
return; const tombstone = { ...current, outcome: 'released', at, ...(i.routeKind === undefined ? {} : { routeKind: i.routeKind }), ...(i.attemptN === undefined ? {} : { attemptN: i.attemptN }), ...(i.signature === undefined ? {} : { signature: i.signature }), ...(i.message === undefined ? {} : { message: i.message }), ...(i.diagnostic === undefined ? {} : { diagnostic: i.diagnostic }), expiresAt: undefined }; delete tombstone.expiresAt; delete tombstone.ownerPid; await write(f, [...all.filter(x => x !== current), tombstone]); });
|
|
301
|
+
}
|
|
302
|
+
/** Token-fenced terminal write. A token may only finish the attempt it claimed. */
|
|
303
|
+
async appendEvidence(i) {
|
|
304
|
+
const f = this.file('evidence'), r = { ...i, location: await canonicalPath(i.location), at: i.at ?? this.clock(), v: 1 };
|
|
305
|
+
const token = i.claimToken;
|
|
306
|
+
if (typeof token !== 'string' || token.length === 0)
|
|
307
|
+
return;
|
|
308
|
+
if (i.kind === 'wake') {
|
|
309
|
+
if (r.outcome !== 'accepted' && r.outcome !== 'failed' && r.outcome !== 'unknown')
|
|
310
|
+
return;
|
|
311
|
+
const outcome = r.outcome;
|
|
312
|
+
await this.updateWake(r, (current) => {
|
|
313
|
+
if (current.outcome !== 'dispatching' && !(current.outcome === 'unknown' && current.unknownSource === 'interrupted' && outcome === 'accepted'))
|
|
314
|
+
return undefined;
|
|
315
|
+
return finishWake(current, outcome, { at: r.at, routeKind: r.routeKind, attemptN: r.attemptN, signature: r.signature, message: r.message, diagnostic: r.diagnostic });
|
|
316
|
+
}, r.at);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
await withFileLock(f + '.lock', LOCK, async () => {
|
|
320
|
+
const all = await read(f, r.at, true);
|
|
321
|
+
const same = (x) => x.kind === r.kind && x.location === r.location && nameKey(x.participant) === nameKey(r.participant) && x.activity === r.activity && x.session === r.session;
|
|
322
|
+
const dispatching = all.findLast((x) => same(x) && x.outcome === 'dispatching');
|
|
323
|
+
const source = dispatching?.claimToken === token ? dispatching : undefined;
|
|
324
|
+
if (source === undefined)
|
|
325
|
+
return;
|
|
326
|
+
await write(f, [...all.filter((x) => x !== source && !(same(x) && x.attemptN === r.attemptN && x.outcome !== 'dispatching' && x.outcome !== 'released')), { ...r, ...(source.nativeDelivery === undefined ? {} : { nativeDelivery: source.nativeDelivery }) }]);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
async listEvidence(i = {}) { const loc = i.location === undefined ? undefined : await canonicalPath(i.location); return (await read(this.file('evidence'), i.now ?? this.clock())).filter(r => (i.includeReleased === true || r.outcome !== 'released') && (!loc || r.location === loc) && (!i.participant || nameKey(r.participant) === nameKey(i.participant)) && (!i.session || r.session === i.session) && (!i.activity || r.activity === i.activity) && (!i.kind || r.kind === i.kind)); }
|
|
330
|
+
async gcEvidence(i) { const f = this.file('evidence'), pending = new Set(i.pendingWakeActivities ?? []); await withFileLock(f + '.lock', LOCK, async () => write(f, (await read(f, this.clock(), true)).filter(r => r.at >= i.before || durableWake(r) && pending.has(r.activity)))); }
|
|
247
331
|
}
|
|
248
332
|
export function createHostLedgerPort(o = {}) { return new FileHostLedgerPort(o); }
|
package/dist/host-ledger.d.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import type { WakeRouteKind } from './model.js';
|
|
2
|
-
|
|
2
|
+
import type { HarnessSessionChannel } from './participant-identity.js';
|
|
3
|
+
export type PresenceChannel = HarnessSessionChannel | 'unknown';
|
|
3
4
|
export interface PresenceRecord {
|
|
4
5
|
readonly location: string;
|
|
5
6
|
readonly participant: string;
|
|
6
7
|
readonly session: string;
|
|
7
8
|
readonly channel: PresenceChannel;
|
|
8
|
-
readonly route?: {
|
|
9
|
-
readonly kind: WakeRouteKind;
|
|
10
|
-
readonly address: Readonly<Record<string, string>>;
|
|
11
|
-
};
|
|
12
9
|
readonly updatedAt?: number;
|
|
13
10
|
readonly epoch?: number;
|
|
11
|
+
readonly cancelledThrough?: number;
|
|
14
12
|
}
|
|
15
13
|
export type PresenceKey = Pick<PresenceRecord, 'location' | 'participant' | 'session' | 'channel'>;
|
|
16
14
|
export interface PresenceLookup {
|
|
@@ -38,12 +36,24 @@ export type PresenceClaimResult = {
|
|
|
38
36
|
readonly record: PresenceRecord;
|
|
39
37
|
readonly error: unknown;
|
|
40
38
|
};
|
|
39
|
+
export type NativeDeliveryEvidence = {
|
|
40
|
+
readonly payload: string;
|
|
41
|
+
readonly epoch: number;
|
|
42
|
+
} & ({
|
|
43
|
+
readonly harness: 'claude';
|
|
44
|
+
readonly endpoint: string;
|
|
45
|
+
} | {
|
|
46
|
+
readonly harness: 'opencode';
|
|
47
|
+
readonly inputId: string;
|
|
48
|
+
});
|
|
41
49
|
export interface EvidenceRecord {
|
|
50
|
+
readonly bindingEpoch?: number;
|
|
51
|
+
readonly nativeDelivery?: NativeDeliveryEvidence;
|
|
42
52
|
readonly location: string;
|
|
43
53
|
readonly participant: string;
|
|
44
54
|
readonly session: string;
|
|
45
55
|
readonly activity: string;
|
|
46
|
-
readonly kind: 'wake' | 'presentation';
|
|
56
|
+
readonly kind: 'wake' | 'presentation' | 'lifecycle';
|
|
47
57
|
readonly outcome: string;
|
|
48
58
|
readonly at?: number;
|
|
49
59
|
readonly expiresAt?: number;
|
|
@@ -53,25 +63,33 @@ export interface EvidenceRecord {
|
|
|
53
63
|
readonly message?: string;
|
|
54
64
|
readonly diagnostic?: unknown;
|
|
55
65
|
readonly claimToken?: string;
|
|
66
|
+
readonly ownerPid?: number;
|
|
67
|
+
readonly unknownSource?: 'interrupted' | 'transport';
|
|
56
68
|
}
|
|
69
|
+
/** Presentation and lifecycle claims only: wake attempts are acquired by claimWakeAttempt. */
|
|
57
70
|
export interface EvidenceClaim {
|
|
58
71
|
readonly location: string;
|
|
59
72
|
readonly participant: string;
|
|
60
73
|
readonly session: string;
|
|
61
74
|
readonly activity: string;
|
|
62
|
-
readonly kind:
|
|
75
|
+
readonly kind: 'presentation' | 'lifecycle';
|
|
63
76
|
readonly leaseMs: number;
|
|
64
|
-
readonly claimToken?: string;
|
|
65
77
|
readonly now?: number;
|
|
66
78
|
}
|
|
67
79
|
/** Release metadata and includeReleased are diagnostic-only; default evidence reads stay behavior-safe. */
|
|
68
|
-
export interface EvidenceRelease
|
|
80
|
+
export interface EvidenceRelease {
|
|
81
|
+
readonly location: string;
|
|
82
|
+
readonly participant: string;
|
|
83
|
+
readonly session: string;
|
|
84
|
+
readonly activity: string;
|
|
85
|
+
readonly kind: EvidenceRecord['kind'];
|
|
69
86
|
readonly claimToken: string;
|
|
70
87
|
readonly routeKind?: WakeRouteKind;
|
|
71
88
|
readonly attemptN?: number;
|
|
72
89
|
readonly signature?: string;
|
|
73
90
|
readonly message?: string;
|
|
74
91
|
readonly diagnostic?: unknown;
|
|
92
|
+
readonly now?: number;
|
|
75
93
|
}
|
|
76
94
|
export interface EvidenceLookup {
|
|
77
95
|
readonly location?: string;
|
|
@@ -91,46 +109,28 @@ export interface WakeAttention {
|
|
|
91
109
|
readonly actIndex: number;
|
|
92
110
|
readonly recipient: string;
|
|
93
111
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
readonly
|
|
97
|
-
readonly
|
|
98
|
-
readonly routeKind
|
|
99
|
-
readonly
|
|
100
|
-
readonly
|
|
112
|
+
/** One wake attempt row carries attention-wide exclusion and session-specific history. */
|
|
113
|
+
export interface WakeAttemptClaimInput {
|
|
114
|
+
readonly attention: WakeAttention;
|
|
115
|
+
readonly session: string;
|
|
116
|
+
readonly routeKind: WakeRouteKind;
|
|
117
|
+
readonly leaseMs: number;
|
|
118
|
+
readonly now?: number;
|
|
101
119
|
}
|
|
102
|
-
export type
|
|
103
|
-
readonly
|
|
104
|
-
readonly
|
|
120
|
+
export type WakeAttemptClaim = {
|
|
121
|
+
readonly status: 'acquired';
|
|
122
|
+
readonly claimToken: string;
|
|
123
|
+
readonly attemptN: number;
|
|
124
|
+
} | {
|
|
125
|
+
readonly status: 'busy';
|
|
126
|
+
readonly record: EvidenceRecord;
|
|
105
127
|
} | {
|
|
106
|
-
readonly
|
|
128
|
+
readonly status: 'terminal';
|
|
129
|
+
readonly record: EvidenceRecord;
|
|
107
130
|
} | {
|
|
108
|
-
readonly
|
|
109
|
-
readonly
|
|
131
|
+
readonly status: 'degraded';
|
|
132
|
+
readonly error: unknown;
|
|
110
133
|
};
|
|
111
|
-
export interface WakeDispatchClaimInput {
|
|
112
|
-
readonly attention: WakeAttention;
|
|
113
|
-
readonly leaseId: string;
|
|
114
|
-
readonly leaseMs: number;
|
|
115
|
-
readonly session?: string;
|
|
116
|
-
readonly at?: number;
|
|
117
|
-
}
|
|
118
|
-
export interface WakeDispatchTransitionInput {
|
|
119
|
-
readonly attention: WakeAttention;
|
|
120
|
-
readonly leaseId: string;
|
|
121
|
-
readonly phase: WakeDispatchLease['phase'];
|
|
122
|
-
readonly leaseMs: number;
|
|
123
|
-
readonly routeKind?: WakeRouteKind;
|
|
124
|
-
readonly attemptN?: number;
|
|
125
|
-
readonly session?: string;
|
|
126
|
-
readonly at?: number;
|
|
127
|
-
}
|
|
128
|
-
export interface WakeDispatchReleaseInput {
|
|
129
|
-
readonly attention: WakeAttention;
|
|
130
|
-
readonly leaseId: string;
|
|
131
|
-
readonly session?: string;
|
|
132
|
-
readonly at?: number;
|
|
133
|
-
}
|
|
134
134
|
export interface WakeAttemptLookup {
|
|
135
135
|
readonly attention?: WakeAttention;
|
|
136
136
|
readonly session?: string;
|
|
@@ -147,8 +147,10 @@ export type ClaimResult = {
|
|
|
147
147
|
readonly error: unknown;
|
|
148
148
|
};
|
|
149
149
|
export interface HostLedgerPort {
|
|
150
|
+
withClaimLock<T>(fn: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
150
151
|
claimPresence(input: PresenceRecord, signal?: AbortSignal): Promise<PresenceClaimResult>;
|
|
151
152
|
ensurePresence(input: PresenceRecord): Promise<PresenceResult>;
|
|
153
|
+
suppressPresence(input: PresenceRecord, through: number): Promise<boolean>;
|
|
152
154
|
removePresence(input: PresenceKey): Promise<void>;
|
|
153
155
|
removePresenceIfUnchanged(input: PresenceRecord): Promise<boolean>;
|
|
154
156
|
listPresence(input: PresenceLookup): Promise<readonly PresenceRecord[]>;
|
|
@@ -159,11 +161,14 @@ export interface HostLedgerPort {
|
|
|
159
161
|
}): Promise<void>;
|
|
160
162
|
listEvidence(input: EvidenceLookup): Promise<readonly EvidenceRecord[]>;
|
|
161
163
|
listWakeAttempts(input?: WakeAttemptLookup): Promise<readonly EvidenceRecord[]>;
|
|
162
|
-
|
|
164
|
+
prepareNativeWake(input: EvidenceRecord & {
|
|
163
165
|
readonly claimToken: string;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
readonly routeKind: WakeRouteKind;
|
|
167
|
+
readonly nativeDelivery: NativeDeliveryEvidence;
|
|
168
|
+
}): Promise<boolean>;
|
|
169
|
+
confirmWakeAdmission(input: EvidenceRecord & {
|
|
170
|
+
readonly claimToken: string;
|
|
171
|
+
}): Promise<boolean>;
|
|
172
|
+
claimWakeAttempt(input: WakeAttemptClaimInput): Promise<WakeAttemptClaim>;
|
|
168
173
|
gcEvidence(input: EvidenceGc): Promise<void>;
|
|
169
174
|
}
|
package/dist/inbox.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type InboxMembership } from './model.js';
|
|
2
|
-
export declare function hostLedgerForEnv(env: NodeJS.ProcessEnv): import("./host-ledger-file-adapter.js").FileHostLedgerPort;
|
|
3
2
|
export interface PendingWaitOptions {
|
|
4
3
|
signal?: AbortSignal;
|
|
5
4
|
/** Local delivery suppression, never persisted as evidence of receipt. */
|
|
@@ -15,5 +14,3 @@ export interface SessionPendingObserver {
|
|
|
15
14
|
}
|
|
16
15
|
/** Session-scoped projection over process-shared storage change detectors. */
|
|
17
16
|
export declare function observeSessionPending(sessionId: string, suppliedEnv?: NodeJS.ProcessEnv): Promise<SessionPendingObserver>;
|
|
18
|
-
/** Finite one-shot consumers retain their API; Pi owns a long-lived observer instead. */
|
|
19
|
-
export declare function waitForSessionPending(sessionId: string, timeoutMs: number, options?: PendingWaitOptions, env?: NodeJS.ProcessEnv): Promise<InboxMembership[]>;
|