@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,72 +0,0 @@
|
|
|
1
|
-
import { gcHostLedger } from '../ledger-maintenance.js';
|
|
2
|
-
import { migrateHostLedger, verifyHostLedger } from '../host-ledger-migration.js';
|
|
3
|
-
import { usage } from './context.js';
|
|
4
|
-
function renderReport(report) {
|
|
5
|
-
const lines = [
|
|
6
|
-
`host ledger: ${report.status}`,
|
|
7
|
-
`root: ${report.rootPath}`,
|
|
8
|
-
`presence: ${report.presence} · evidence: ${report.evidence}`,
|
|
9
|
-
];
|
|
10
|
-
if (report.presenceRevision !== undefined) {
|
|
11
|
-
lines.push(`revisions: presence ${report.presenceRevision} · evidence ${report.evidenceRevision}`);
|
|
12
|
-
}
|
|
13
|
-
if (report.status === 'upgraded')
|
|
14
|
-
lines.push('SQLite schema upgraded in place; records, order and revisions preserved.');
|
|
15
|
-
if (report.ready !== undefined)
|
|
16
|
-
lines.push(`readiness: ${report.ready ? 'ready' : 'not ready'}`);
|
|
17
|
-
if (report.sources !== undefined) {
|
|
18
|
-
for (const kind of ['presence', 'evidence']) {
|
|
19
|
-
const source = report.sources[kind];
|
|
20
|
-
lines.push(`${kind}.ndjsonl: ${source.records} accepted · ${source.ignored} ignored · ${source.rejected} rejected · sha256 ${source.sha256 ?? 'absent'}`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
if (report.status === 'legacy') {
|
|
24
|
-
if (report.ready)
|
|
25
|
-
lines.push('Stop agent sessions and MCP services using Square with this ledger before conversion.', 'Keep them stopped until migration and verification succeed; then start them with the updated Square version.', 'Next: migrate, then verify.', 'square ledger migrate', 'square ledger verify');
|
|
26
|
-
else
|
|
27
|
-
lines.push('Unsafe legacy records; conversion refused. Preserve the sources and resolve the rejected records before retrying verification.');
|
|
28
|
-
}
|
|
29
|
-
if (report.status === 'migrated' || report.status === 'upgraded') {
|
|
30
|
-
lines.push('Keep affected sessions and MCP services stopped until verification succeeds.', 'After verification, start them with the updated Square version.', 'square ledger verify');
|
|
31
|
-
}
|
|
32
|
-
return lines.join('\n') + '\n';
|
|
33
|
-
}
|
|
34
|
-
/** Counts only: unreadable locations are summarized, never printed, so no payload or credential can leak. */
|
|
35
|
-
function renderMaintenance(report) {
|
|
36
|
-
return [
|
|
37
|
-
`host ledger: gc${report.dryRun ? ' (dry run)' : ''}`,
|
|
38
|
-
`eligible: ${report.eligible} · retired: ${report.retired} · cas misses: ${report.casMisses}`,
|
|
39
|
-
`unreadable artifacts: ${report.unavailableArtifacts.length}`,
|
|
40
|
-
...(report.dryRun ? ['Dry run: nothing retired; ledger and artifacts unchanged.'] : []),
|
|
41
|
-
'',
|
|
42
|
-
].join('\n');
|
|
43
|
-
}
|
|
44
|
-
export const ledgerCommand = {
|
|
45
|
-
parse(argv, context) {
|
|
46
|
-
if (argv[0] === 'gc') {
|
|
47
|
-
if (argv.length === 1)
|
|
48
|
-
return { action: 'gc', dryRun: false };
|
|
49
|
-
if (argv.length === 2 && argv[1] === '--dry-run')
|
|
50
|
-
return { action: 'gc', dryRun: true };
|
|
51
|
-
return usage(context.command);
|
|
52
|
-
}
|
|
53
|
-
if (argv.length !== 1 || (argv[0] !== 'verify' && argv[0] !== 'migrate'))
|
|
54
|
-
usage(context.command);
|
|
55
|
-
return { action: argv[0] };
|
|
56
|
-
},
|
|
57
|
-
execute(intent, context) {
|
|
58
|
-
if (intent.action === 'gc')
|
|
59
|
-
return gcHostLedger({ env: context.env, dryRun: intent.dryRun });
|
|
60
|
-
const options = { env: context.env };
|
|
61
|
-
return intent.action === 'verify' ? verifyHostLedger(options) : migrateHostLedger(options);
|
|
62
|
-
},
|
|
63
|
-
present(result) {
|
|
64
|
-
if ('dryRun' in result) {
|
|
65
|
-
process.stdout.write(renderMaintenance(result));
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
process.stdout.write(renderReport(result));
|
|
69
|
-
if (result.ready === false)
|
|
70
|
-
process.exitCode = 2;
|
|
71
|
-
},
|
|
72
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { type VersionObserver } from './file-changes.js';
|
|
2
|
-
import type { HostLedgerPort, PresenceRecord, PresenceKey, PresenceLookup, PresenceResult, PresenceClaimResult, EvidenceRecord, EvidenceClaim, EvidenceRelease, EvidenceLookup, NativeTokenLookup, NativeTokenResult, ClaimResult, WakeAttemptClaim, WakeAttemptClaimInput, WakeAttemptLookup } from './host-ledger.js';
|
|
3
|
-
export interface HostLedgerFileAdapterOptions {
|
|
4
|
-
rootPath?: string;
|
|
5
|
-
now?: () => number;
|
|
6
|
-
}
|
|
7
|
-
/** Existing port surface; each operation owns a short-lived SQLite connection. */
|
|
8
|
-
export declare class FileHostLedgerPort implements HostLedgerPort {
|
|
9
|
-
private readonly root;
|
|
10
|
-
private readonly clock;
|
|
11
|
-
constructor(o?: HostLedgerFileAdapterOptions);
|
|
12
|
-
observeChanges(): Promise<{
|
|
13
|
-
presence: VersionObserver<string>;
|
|
14
|
-
evidence: VersionObserver<string>;
|
|
15
|
-
}>;
|
|
16
|
-
nextExpiry(session: string): Promise<number>;
|
|
17
|
-
withClaimLock<T>(fn: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
18
|
-
claimPresence(i: PresenceRecord, signal?: AbortSignal): Promise<PresenceClaimResult>;
|
|
19
|
-
ensurePresence(i: PresenceRecord): Promise<PresenceResult>;
|
|
20
|
-
suppressPresence(i: PresenceRecord, through: number): Promise<boolean>;
|
|
21
|
-
removePresence(i: PresenceKey): Promise<void>;
|
|
22
|
-
removePresenceIfUnchanged(i: PresenceRecord): Promise<boolean>;
|
|
23
|
-
listPresence(i?: PresenceLookup): Promise<readonly PresenceRecord[]>;
|
|
24
|
-
claimWakeAttempt(i: WakeAttemptClaimInput): Promise<WakeAttemptClaim>;
|
|
25
|
-
private updateWake;
|
|
26
|
-
listWakeAttempts(i?: WakeAttemptLookup): Promise<readonly EvidenceRecord[]>;
|
|
27
|
-
prepareNativeWake(i: EvidenceRecord & {
|
|
28
|
-
readonly claimToken: string;
|
|
29
|
-
readonly routeKind: import('./model.js').WakeRouteKind;
|
|
30
|
-
readonly nativeDelivery: import('./host-ledger.js').NativeDeliveryEvidence;
|
|
31
|
-
}): Promise<boolean>;
|
|
32
|
-
confirmWakeAdmission(i: EvidenceRecord & {
|
|
33
|
-
readonly claimToken: string;
|
|
34
|
-
}): Promise<boolean>;
|
|
35
|
-
observeNativeDelivery(i: EvidenceRecord & {
|
|
36
|
-
readonly claimToken: string;
|
|
37
|
-
}, observation: import('./host-ledger.js').NativeDeliveryObservation): Promise<boolean>;
|
|
38
|
-
claimEvidence(i: EvidenceClaim): Promise<ClaimResult>;
|
|
39
|
-
releaseEvidence(i: EvidenceRelease): Promise<void>;
|
|
40
|
-
appendEvidence(i: EvidenceRecord & {
|
|
41
|
-
readonly claimToken: string;
|
|
42
|
-
}): Promise<void>;
|
|
43
|
-
listEvidence(i?: EvidenceLookup): Promise<readonly EvidenceRecord[]>;
|
|
44
|
-
retireEvidenceIfUnchanged(expected: EvidenceRecord): Promise<boolean>;
|
|
45
|
-
lookupNativeTokens(input: NativeTokenLookup): Promise<NativeTokenResult>;
|
|
46
|
-
}
|
|
47
|
-
export declare function createHostLedgerPort(o?: HostLedgerFileAdapterOptions): FileHostLedgerPort;
|
|
@@ -1,384 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
-
import { withFileLock as acquireFileLock } from './file-lock.js';
|
|
6
|
-
import { nameKey } from './model.js';
|
|
7
|
-
import { formatActivityId } from './square-core.js';
|
|
8
|
-
import { hostLedgerRoot } from './host-ledger-root.js';
|
|
9
|
-
import { observeFileMetadata, observeFileVersion } from './file-changes.js';
|
|
10
|
-
import { canonicalPath, canonicalPathSync } from './canonical-path.js';
|
|
11
|
-
import { LEDGER_FILENAME, LEDGER_RETENTION, withLedger, readLedgerRevisions } from './host-ledger-sqlite.js';
|
|
12
|
-
const LOCK = { retryMs: 10 };
|
|
13
|
-
// The claim fence deliberately spans asynchronous artifact/native work, never a ledger transaction.
|
|
14
|
-
async function withFileLock(file, options, fn) {
|
|
15
|
-
if (options.signal?.aborted)
|
|
16
|
-
throw options.signal.reason ?? new Error('File lock acquisition aborted');
|
|
17
|
-
const directory = path.dirname(file);
|
|
18
|
-
await fs.mkdir(directory, { recursive: true });
|
|
19
|
-
try {
|
|
20
|
-
await fs.writeFile(path.join(directory, '.gitignore'), '*\n', { flag: 'wx' });
|
|
21
|
-
}
|
|
22
|
-
catch (error) {
|
|
23
|
-
if (error.code !== 'EEXIST')
|
|
24
|
-
throw error;
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
return await acquireFileLock(file, options, fn);
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
if (options.signal?.aborted)
|
|
31
|
-
throw options.signal.reason ?? error;
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function identity(v) {
|
|
36
|
-
return JSON.stringify([v.location, nameKey(v.participant), v.session, v.channel, v.activity, v.kind]);
|
|
37
|
-
}
|
|
38
|
-
function durableWake(record) {
|
|
39
|
-
const row = record;
|
|
40
|
-
return row.kind === 'wake' && (['accepted', 'unknown', 'dispatching'].includes(row.outcome ?? '') || row.nativeDelivery?.harness === 'claude');
|
|
41
|
-
}
|
|
42
|
-
function finishWake(current, outcome, details = {}) {
|
|
43
|
-
const { expiresAt: _expiresAt, ownerPid: _ownerPid, unknownSource: _unknownSource, ...rest } = current;
|
|
44
|
-
return { ...rest, ...details, kind: 'wake', outcome, ...(outcome === 'unknown' ? { unknownSource: details.unknownSource ?? 'transport' } : {}) };
|
|
45
|
-
}
|
|
46
|
-
function processAlive(pid) {
|
|
47
|
-
if (pid === undefined)
|
|
48
|
-
return undefined;
|
|
49
|
-
try {
|
|
50
|
-
process.kill(pid, 0);
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
return error.code === 'ESRCH' ? false : true;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const revisionReaders = new Map();
|
|
58
|
-
async function revisions(root, entry, signal) {
|
|
59
|
-
for (;;) {
|
|
60
|
-
const metadata = await entry.metadata.read(signal);
|
|
61
|
-
if (entry.cached?.metadata === metadata)
|
|
62
|
-
return entry.cached.revisions;
|
|
63
|
-
if (entry.pending) {
|
|
64
|
-
await entry.pending;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
entry.pending = readLedgerRevisions(root, entry.controller.signal).then((value) => {
|
|
68
|
-
// Never associate a snapshot with metadata obtained after the read.
|
|
69
|
-
entry.cached = { metadata, revisions: value };
|
|
70
|
-
}).finally(() => { entry.pending = undefined; });
|
|
71
|
-
await entry.pending;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
async function observeLedger(root) {
|
|
75
|
-
let shared = revisionReaders.get(root);
|
|
76
|
-
if (!shared) {
|
|
77
|
-
shared = observeFileMetadata(path.join(root, LEDGER_FILENAME)).then((metadata) => ({ metadata, refs: 0, controller: new AbortController() }));
|
|
78
|
-
revisionReaders.set(root, shared);
|
|
79
|
-
void shared.catch(() => { if (revisionReaders.get(root) === shared)
|
|
80
|
-
revisionReaders.delete(root); });
|
|
81
|
-
}
|
|
82
|
-
const entry = await shared;
|
|
83
|
-
// A previous last subscriber may have closed while this acquisition awaited metadata.
|
|
84
|
-
if (revisionReaders.get(root) !== shared)
|
|
85
|
-
return observeLedger(root);
|
|
86
|
-
entry.refs += 2;
|
|
87
|
-
const release = () => { if (--entry.refs === 0) {
|
|
88
|
-
entry.controller.abort(new Error('Host ledger monitor closed'));
|
|
89
|
-
entry.metadata.close();
|
|
90
|
-
revisionReaders.delete(root);
|
|
91
|
-
} };
|
|
92
|
-
async function category(kind) {
|
|
93
|
-
const observer = await observeFileVersion(path.join(root, LEDGER_FILENAME), `host-ledger-${kind}`, async (_file, signal) => {
|
|
94
|
-
const pair = await revisions(root, entry, signal);
|
|
95
|
-
return pair === undefined ? 'missing' : String(pair[kind]);
|
|
96
|
-
});
|
|
97
|
-
let closed = false;
|
|
98
|
-
return { get generation() { return observer.generation; }, hinted: observer.hinted, read: observer.read, changed: observer.changed,
|
|
99
|
-
close() { if (!closed) {
|
|
100
|
-
closed = true;
|
|
101
|
-
observer.close();
|
|
102
|
-
release();
|
|
103
|
-
} } };
|
|
104
|
-
}
|
|
105
|
-
let presence;
|
|
106
|
-
try {
|
|
107
|
-
presence = await category('presence');
|
|
108
|
-
return { presence, evidence: await category('evidence') };
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
if (presence)
|
|
112
|
-
presence.close();
|
|
113
|
-
else
|
|
114
|
-
release();
|
|
115
|
-
release();
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
/** Existing port surface; each operation owns a short-lived SQLite connection. */
|
|
120
|
-
export class FileHostLedgerPort {
|
|
121
|
-
root;
|
|
122
|
-
clock;
|
|
123
|
-
constructor(o = {}) {
|
|
124
|
-
this.root = canonicalPathSync(o.rootPath ?? hostLedgerRoot());
|
|
125
|
-
this.clock = o.now ?? Date.now;
|
|
126
|
-
}
|
|
127
|
-
observeChanges() { return observeLedger(this.root); }
|
|
128
|
-
async nextExpiry(session) {
|
|
129
|
-
const now = this.clock();
|
|
130
|
-
return withLedger(this.root, false, (tx) => {
|
|
131
|
-
const records = [...tx.presence({ session }, now, true), ...tx.evidence({ session, includeReleased: true }, now, true)];
|
|
132
|
-
let next = Infinity;
|
|
133
|
-
for (const { record } of records) {
|
|
134
|
-
const at = 'updatedAt' in record ? record.updatedAt : 'at' in record ? record.at : undefined;
|
|
135
|
-
if (typeof at === 'number') {
|
|
136
|
-
if (at > now)
|
|
137
|
-
next = Math.min(next, at);
|
|
138
|
-
if (at + LEDGER_RETENTION + 1 > now && !durableWake(record))
|
|
139
|
-
next = Math.min(next, at + LEDGER_RETENTION + 1);
|
|
140
|
-
}
|
|
141
|
-
if ('expiresAt' in record && typeof record.expiresAt === 'number' && record.expiresAt > now)
|
|
142
|
-
next = Math.min(next, record.expiresAt);
|
|
143
|
-
}
|
|
144
|
-
return next;
|
|
145
|
-
}, Infinity);
|
|
146
|
-
}
|
|
147
|
-
withClaimLock(fn, signal) {
|
|
148
|
-
return withFileLock(path.join(this.root, 'presence-claim.lock'), { ...LOCK, signal }, fn);
|
|
149
|
-
}
|
|
150
|
-
async claimPresence(i, signal) {
|
|
151
|
-
const location = await canonicalPath(i.location);
|
|
152
|
-
const record = { ...i, location, updatedAt: i.updatedAt ?? this.clock(), v: 1 };
|
|
153
|
-
try {
|
|
154
|
-
return await this.withClaimLock(() => withLedger(this.root, true, (tx) => {
|
|
155
|
-
const existing = tx.presence({ location, participant: record.participant }, this.clock(), true)
|
|
156
|
-
.map(({ record }) => record).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
|
|
157
|
-
if (existing !== undefined)
|
|
158
|
-
return { status: existing.session === record.session ? 'owned' : 'busy', record: existing };
|
|
159
|
-
tx.publish('presence', this.clock());
|
|
160
|
-
tx.insert('presence', record);
|
|
161
|
-
return { status: 'acquired', record };
|
|
162
|
-
}, { status: 'degraded', record, error: new Error('Missing writable host ledger') }, signal), signal);
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
return { status: 'degraded', record, error };
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
async ensurePresence(i) {
|
|
169
|
-
const record = { ...i, location: await canonicalPath(i.location), updatedAt: i.updatedAt ?? this.clock(), v: 1 };
|
|
170
|
-
try {
|
|
171
|
-
await withLedger(this.root, true, (tx) => {
|
|
172
|
-
const rows = tx.presence({ location: record.location, participant: record.participant, session: record.session, channel: record.channel }, this.clock(), true).filter((row) => identity(row.record) === identity(record));
|
|
173
|
-
const current = rows.at(-1)?.record;
|
|
174
|
-
const cancelledThrough = current?.epoch === record.epoch ? Math.max(current?.cancelledThrough ?? -1, record.cancelledThrough ?? -1) : record.cancelledThrough ?? -1;
|
|
175
|
-
tx.publish('presence', this.clock());
|
|
176
|
-
tx.remove('presence', rows);
|
|
177
|
-
tx.insert('presence', { ...record, ...(cancelledThrough < 0 ? {} : { cancelledThrough }) });
|
|
178
|
-
}, undefined);
|
|
179
|
-
return { status: 'ensured', record };
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
return { status: 'degraded', record, error };
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
async suppressPresence(i, through) {
|
|
186
|
-
const record = { ...i, location: await canonicalPath(i.location) };
|
|
187
|
-
return withLedger(this.root, true, (tx) => {
|
|
188
|
-
const current = tx.presence({ location: record.location, participant: record.participant, session: record.session, channel: record.channel }, this.clock(), true).findLast((row) => identity(row.record) === identity(record) && row.record.epoch === record.epoch);
|
|
189
|
-
if (!current)
|
|
190
|
-
return false;
|
|
191
|
-
tx.publish('presence', this.clock());
|
|
192
|
-
tx.replace('presence', current.seq, { ...current.record, cancelledThrough: Math.max(current.record.cancelledThrough ?? -1, through) });
|
|
193
|
-
return true;
|
|
194
|
-
}, false);
|
|
195
|
-
}
|
|
196
|
-
async removePresence(i) {
|
|
197
|
-
const record = { ...i, location: await canonicalPath(i.location) };
|
|
198
|
-
await withLedger(this.root, true, (tx) => {
|
|
199
|
-
const rows = tx.presence({ location: record.location, participant: record.participant, session: record.session, channel: record.channel }, this.clock(), true).filter((row) => identity(row.record) === identity(record));
|
|
200
|
-
tx.publish('presence', this.clock());
|
|
201
|
-
tx.remove('presence', rows);
|
|
202
|
-
}, undefined);
|
|
203
|
-
}
|
|
204
|
-
async removePresenceIfUnchanged(i) {
|
|
205
|
-
const record = { ...i, location: await canonicalPath(i.location) };
|
|
206
|
-
return withLedger(this.root, true, (tx) => {
|
|
207
|
-
const current = tx.presence({ location: record.location, participant: record.participant, session: record.session, channel: record.channel }, this.clock(), true).findLast(({ record: row }) => identity(row) === identity(record) && (row.epoch ?? undefined) === (record.epoch ?? undefined) && (row.updatedAt ?? undefined) === (record.updatedAt ?? undefined));
|
|
208
|
-
if (!current)
|
|
209
|
-
return false;
|
|
210
|
-
tx.publish('presence', this.clock());
|
|
211
|
-
tx.remove('presence', [current]);
|
|
212
|
-
return true;
|
|
213
|
-
}, false);
|
|
214
|
-
}
|
|
215
|
-
async listPresence(i = {}) {
|
|
216
|
-
const location = i.location === undefined ? undefined : await canonicalPath(i.location);
|
|
217
|
-
return withLedger(this.root, false, (tx) => tx.presence({ ...i, location, participant: i.participant || undefined, session: i.session || undefined }, i.now ?? this.clock()).map(({ record }) => {
|
|
218
|
-
const { scope: _scope, ...row } = record;
|
|
219
|
-
return row;
|
|
220
|
-
}), []);
|
|
221
|
-
}
|
|
222
|
-
async claimWakeAttempt(i) {
|
|
223
|
-
const location = await canonicalPath(i.attention.squarePath), activity = formatActivityId(i.attention.actIndex), at = i.now ?? this.clock();
|
|
224
|
-
try {
|
|
225
|
-
return await withLedger(this.root, true, (tx) => {
|
|
226
|
-
const attention = tx.evidence({ location, participant: i.attention.recipient, activity, kind: 'wake', includeReleased: true }, at, true);
|
|
227
|
-
const terminal = attention.findLast(({ record }) => record.outcome === 'accepted') ?? attention.findLast(({ record }) => record.outcome === 'unknown');
|
|
228
|
-
if (terminal)
|
|
229
|
-
return { status: 'terminal', record: terminal.record };
|
|
230
|
-
const interrupted = attention.filter(({ record }) => record.outcome === 'dispatching');
|
|
231
|
-
const busy = interrupted.find(({ record }) => (record.expiresAt ?? 0) > at && processAlive(record.ownerPid) !== false);
|
|
232
|
-
if (busy)
|
|
233
|
-
return { status: 'busy', record: busy.record };
|
|
234
|
-
const nextNumber = (session) => attention.filter(({ record }) => record.session === session).reduce((highest, { record }) => Math.max(highest, record.attemptN ?? 0), 0) + 1;
|
|
235
|
-
tx.publish('evidence', at);
|
|
236
|
-
if (interrupted.length) {
|
|
237
|
-
const recovered = interrupted.map(({ record }) => ({ ...finishWake(record, 'unknown', {
|
|
238
|
-
unknownSource: 'interrupted', signature: 'worker_interrupted_during_dispatch', message: 'The notification worker ended after dispatch began; transport acceptance is unknown.',
|
|
239
|
-
}), routeKind: record.routeKind ?? i.routeKind, attemptN: record.attemptN ?? nextNumber(record.session), v: 1 }));
|
|
240
|
-
interrupted.forEach((row, index) => tx.replace('evidence', row.seq, recovered[index]));
|
|
241
|
-
return { status: 'terminal', record: recovered[0] };
|
|
242
|
-
}
|
|
243
|
-
const attemptN = nextNumber(i.session), claimToken = `${process.pid}-${at}-${randomUUID()}`;
|
|
244
|
-
tx.insert('evidence', { v: 1, at, location, participant: i.attention.recipient, session: i.session, activity, kind: 'wake', outcome: 'dispatching', routeKind: i.routeKind, attemptN, claimToken, ownerPid: process.pid, expiresAt: at + i.leaseMs });
|
|
245
|
-
return { status: 'acquired', claimToken, attemptN };
|
|
246
|
-
}, { status: 'degraded', error: new Error('Missing writable host ledger') });
|
|
247
|
-
}
|
|
248
|
-
catch (error) {
|
|
249
|
-
return { status: 'degraded', error };
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
async updateWake(input, update, at = this.clock()) {
|
|
253
|
-
if (!input.claimToken)
|
|
254
|
-
return false;
|
|
255
|
-
const location = await canonicalPath(input.location);
|
|
256
|
-
return withLedger(this.root, true, (tx) => {
|
|
257
|
-
const current = tx.evidence({ location, participant: input.participant, activity: input.activity, session: input.session, kind: 'wake', claimToken: input.claimToken, includeReleased: true }, at, true).at(-1);
|
|
258
|
-
const next = current && update(current.record);
|
|
259
|
-
if (!next || !current)
|
|
260
|
-
return false;
|
|
261
|
-
if (next !== current.record) {
|
|
262
|
-
tx.publish('evidence', at);
|
|
263
|
-
tx.replace('evidence', current.seq, { ...next, v: 1 });
|
|
264
|
-
}
|
|
265
|
-
return true;
|
|
266
|
-
}, false);
|
|
267
|
-
}
|
|
268
|
-
async listWakeAttempts(i = {}) {
|
|
269
|
-
const attention = i.attention;
|
|
270
|
-
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 });
|
|
271
|
-
}
|
|
272
|
-
async prepareNativeWake(i) {
|
|
273
|
-
return this.updateWake(i, (current) => current.outcome === 'dispatching' ? { ...current, nativeDelivery: i.nativeDelivery, routeKind: i.routeKind, attemptN: i.attemptN } : undefined);
|
|
274
|
-
}
|
|
275
|
-
async confirmWakeAdmission(i) {
|
|
276
|
-
return this.updateWake(i, (current) => {
|
|
277
|
-
if (!current.nativeDelivery || !['dispatching', 'unknown', 'accepted'].includes(current.outcome))
|
|
278
|
-
return undefined;
|
|
279
|
-
return current.outcome === 'accepted' ? current : finishWake(current, 'accepted', { signature: 'native_queue_confirmed', message: undefined, diagnostic: undefined });
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
async observeNativeDelivery(i, observation) {
|
|
283
|
-
return this.updateWake(i, (current) => {
|
|
284
|
-
const native = current.nativeDelivery;
|
|
285
|
-
if (native?.harness !== 'claude' || native.messageId !== observation.messageId)
|
|
286
|
-
return undefined;
|
|
287
|
-
const observations = current.nativeObservations ?? [];
|
|
288
|
-
if (observations.some((prior) => prior.eventId === observation.eventId))
|
|
289
|
-
return current;
|
|
290
|
-
// Native custody is diagnostic evidence, never permission to retry or a queue receipt.
|
|
291
|
-
return { ...current, nativeObservations: [...observations, observation] };
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
async claimEvidence(i) {
|
|
295
|
-
if (i.kind === 'wake')
|
|
296
|
-
return { status: 'degraded', error: new Error('wake attempts are claimed with claimWakeAttempt') };
|
|
297
|
-
const at = i.now ?? this.clock(), { leaseMs, ...claim } = i;
|
|
298
|
-
const record = { ...claim, location: await canonicalPath(i.location), outcome: 'dispatching', at, expiresAt: at + leaseMs, v: 1 };
|
|
299
|
-
try {
|
|
300
|
-
return await withLedger(this.root, true, (tx) => {
|
|
301
|
-
const matching = tx.evidence({ ...record, includeReleased: true }, at, true);
|
|
302
|
-
const old = matching.findLast(({ record }) => record.outcome === 'dispatching');
|
|
303
|
-
if (old && typeof old.record.expiresAt === 'number' && old.record.expiresAt > at)
|
|
304
|
-
return { status: 'busy', record: old.record };
|
|
305
|
-
const presented = matching.findLast(({ record }) => record.outcome === 'presented');
|
|
306
|
-
if (record.kind === 'presentation' && presented)
|
|
307
|
-
return { status: 'delivered', record: presented.record };
|
|
308
|
-
const claimToken = `${process.pid}-${at}-${Math.random().toString(36).slice(2)}`;
|
|
309
|
-
tx.publish('evidence', at);
|
|
310
|
-
tx.remove('evidence', matching.filter(({ record }) => record.outcome === 'dispatching' || record.outcome === 'released'));
|
|
311
|
-
tx.insert('evidence', { ...record, claimToken });
|
|
312
|
-
return { status: 'acquired', claimToken };
|
|
313
|
-
}, { status: 'degraded', error: new Error('Missing writable host ledger') });
|
|
314
|
-
}
|
|
315
|
-
catch (error) {
|
|
316
|
-
return { status: 'degraded', error };
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
async releaseEvidence(i) {
|
|
320
|
-
if (i.kind === 'wake') {
|
|
321
|
-
const { signature, message, diagnostic, routeKind, attemptN } = i;
|
|
322
|
-
const details = Object.fromEntries(Object.entries({ signature, message, diagnostic, routeKind, attemptN }).filter(([, value]) => value !== undefined));
|
|
323
|
-
await this.updateWake(i, (current) => current.outcome === 'dispatching' ? finishWake(current, 'released', { ...details, at: i.now ?? this.clock() }) : undefined, i.now);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
const location = await canonicalPath(i.location), at = i.now ?? this.clock();
|
|
327
|
-
await withLedger(this.root, true, (tx) => {
|
|
328
|
-
const current = tx.evidence({ ...i, location, includeReleased: true, claimToken: undefined }, at, true).findLast(({ record }) => record.outcome === 'dispatching');
|
|
329
|
-
if (!current || current.record.claimToken !== i.claimToken)
|
|
330
|
-
return;
|
|
331
|
-
const { expiresAt: _expiry, ownerPid: _owner, ...rest } = current.record;
|
|
332
|
-
const details = Object.fromEntries(['routeKind', 'attemptN', 'signature', 'message', 'diagnostic'].filter((field) => i[field] !== undefined).map((field) => [field, i[field]]));
|
|
333
|
-
tx.publish('evidence', at);
|
|
334
|
-
tx.remove('evidence', [current]);
|
|
335
|
-
tx.insert('evidence', { ...rest, outcome: 'released', at, ...details });
|
|
336
|
-
}, undefined);
|
|
337
|
-
}
|
|
338
|
-
async appendEvidence(i) {
|
|
339
|
-
const record = { ...i, location: await canonicalPath(i.location), at: i.at ?? this.clock(), v: 1 };
|
|
340
|
-
if (typeof i.claimToken !== 'string' || !i.claimToken.length)
|
|
341
|
-
return;
|
|
342
|
-
if (i.kind === 'wake') {
|
|
343
|
-
if (!['accepted', 'failed', 'unknown'].includes(record.outcome))
|
|
344
|
-
return;
|
|
345
|
-
const outcome = record.outcome;
|
|
346
|
-
await this.updateWake(record, (current) => {
|
|
347
|
-
if (current.outcome !== 'dispatching' && !(current.outcome === 'unknown' && current.unknownSource === 'interrupted' && outcome === 'accepted'))
|
|
348
|
-
return undefined;
|
|
349
|
-
return finishWake(current, outcome, { at: record.at, routeKind: record.routeKind, attemptN: record.attemptN, signature: record.signature, message: record.message, diagnostic: record.diagnostic });
|
|
350
|
-
}, record.at);
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
await withLedger(this.root, true, (tx) => {
|
|
354
|
-
const matching = tx.evidence({ ...record, claimToken: undefined, includeReleased: true }, record.at, true);
|
|
355
|
-
const source = matching.findLast(({ record }) => record.outcome === 'dispatching');
|
|
356
|
-
if (!source || source.record.claimToken !== record.claimToken)
|
|
357
|
-
return;
|
|
358
|
-
tx.publish('evidence', record.at);
|
|
359
|
-
tx.remove('evidence', matching.filter((row) => row === source || row.record.attemptN === record.attemptN && row.record.outcome !== 'dispatching' && row.record.outcome !== 'released'));
|
|
360
|
-
tx.insert('evidence', { ...record, ...(source.record.nativeDelivery === undefined ? {} : { nativeDelivery: source.record.nativeDelivery }) });
|
|
361
|
-
}, undefined);
|
|
362
|
-
}
|
|
363
|
-
async listEvidence(i = {}) {
|
|
364
|
-
const location = i.location === undefined ? undefined : await canonicalPath(i.location);
|
|
365
|
-
return withLedger(this.root, false, (tx) => tx.evidence({ ...i, location, participant: i.participant || undefined, session: i.session || undefined, activity: i.activity || undefined, kind: i.kind || undefined }, i.now ?? this.clock()).map(({ record }) => record), []);
|
|
366
|
-
}
|
|
367
|
-
async retireEvidenceIfUnchanged(expected) {
|
|
368
|
-
if (expected.outcome === 'dispatching')
|
|
369
|
-
return false;
|
|
370
|
-
// Compare the persisted coordinate verbatim; maintenance canonicalizes only its artifact proof.
|
|
371
|
-
return withLedger(this.root, true, (tx) => {
|
|
372
|
-
const current = tx.evidence({ location: expected.location, participant: expected.participant, session: expected.session, activity: expected.activity, kind: expected.kind,
|
|
373
|
-
claimToken: expected.claimToken, includeExpired: true, includeReleased: true }, this.clock(), true)
|
|
374
|
-
.find(({ record }) => isDeepStrictEqual(record, expected));
|
|
375
|
-
return current !== undefined && tx.retireEvidence(current);
|
|
376
|
-
}, false);
|
|
377
|
-
}
|
|
378
|
-
lookupNativeTokens(input) {
|
|
379
|
-
if (!input.tokens.length)
|
|
380
|
-
return Promise.resolve({ deliveries: [], retired: [] });
|
|
381
|
-
return withLedger(this.root, false, tx => tx.nativeTokens(input), { deliveries: [], retired: [] });
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
export function createHostLedgerPort(o = {}) { return new FileHostLedgerPort(o); }
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
type Kind = 'presence' | 'evidence';
|
|
2
|
-
export interface LedgerSourceReport {
|
|
3
|
-
records: number;
|
|
4
|
-
ignored: number;
|
|
5
|
-
rejected: number;
|
|
6
|
-
sha256: string | null;
|
|
7
|
-
}
|
|
8
|
-
export interface HostLedgerReport {
|
|
9
|
-
status: 'empty' | 'legacy' | 'sqlite' | 'migrated' | 'upgraded';
|
|
10
|
-
rootPath: string;
|
|
11
|
-
presence: number;
|
|
12
|
-
evidence: number;
|
|
13
|
-
presenceRevision?: number;
|
|
14
|
-
evidenceRevision?: number;
|
|
15
|
-
sources?: Record<Kind, LedgerSourceReport>;
|
|
16
|
-
ready?: boolean;
|
|
17
|
-
}
|
|
18
|
-
export interface HostLedgerMaintenanceOptions {
|
|
19
|
-
rootPath?: string;
|
|
20
|
-
env?: NodeJS.ProcessEnv;
|
|
21
|
-
signal?: AbortSignal;
|
|
22
|
-
}
|
|
23
|
-
/** Read-only readiness/integrity check. Never creates a root, lock, or database. */
|
|
24
|
-
export declare function verifyHostLedger(options?: HostLedgerMaintenanceOptions): Promise<HostLedgerReport>;
|
|
25
|
-
/** Offline only: locks fence compatible writers, but do not prove hosts have stopped. */
|
|
26
|
-
export declare function migrateHostLedger(options?: HostLedgerMaintenanceOptions): Promise<HostLedgerReport>;
|
|
27
|
-
export {};
|