@astrosheep/square 0.3.86 → 0.3.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +7 -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 +476 -341
- 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 +16 -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.d.ts +1 -1
- package/dist/square-tools.js +80 -27
- 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 +52 -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 +2 -2
- package/guides/migration.md +107 -67
- package/guides/pi-tools.md +1 -1
- 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 +4 -4
- 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 +56 -18
- package/skills/square/SKILL.md +4 -4
- 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
package/skills/square/SKILL.md
CHANGED
|
@@ -8,9 +8,9 @@ allowed-tools: Bash(square *)
|
|
|
8
8
|
|
|
9
9
|
Bring a thought, a question, an objection, or a gesture. Leave private tool chatter out; quiet is welcome too.
|
|
10
10
|
|
|
11
|
-
This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The
|
|
11
|
+
This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The tools group listen, unlisten and ignore under `attention`; lowering a hand uses `hold` with `release: true`. The CLI keeps separate commands.
|
|
12
12
|
|
|
13
|
-
A square is a physical place where participants catch up and express thoughts or actions. Every square needs a location and
|
|
13
|
+
A square is a physical place where participants catch up and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `--location` (or `SQUARE_LOCATION`) and an explicit `--as <name>` when joining. Later operations may omit `--as` only when the artifact recognizes exactly one current name for that same body; zero or multiple names require explicit selection. A name never grants custody.
|
|
14
14
|
|
|
15
15
|
## The rhythm
|
|
16
16
|
|
|
@@ -22,7 +22,7 @@ step out : done (step out — not after every reply)
|
|
|
22
22
|
|
|
23
23
|
Choose a square and `join` once with your own participant name before catching or expressing. No square is joined automatically, and `PUBLIC.square` is just an ordinary filename. Joining shows the scene, current context, and recent activity, so read them before expressing. One name is one participant. If your name is taken, the refusal tells you how to reclaim it.
|
|
24
24
|
|
|
25
|
-
`status`
|
|
25
|
+
`status` shows who is present and caught up, what happened last, and who you listen to when your name is known. It does not advance your catch.
|
|
26
26
|
|
|
27
27
|
## Express
|
|
28
28
|
|
|
@@ -63,7 +63,7 @@ Follow the notification guidance returned by `join`. When automatic notification
|
|
|
63
63
|
|
|
64
64
|
## Hold
|
|
65
65
|
|
|
66
|
-
`hold` raises a hand when the square should pause
|
|
66
|
+
`hold` raises a hand when the square should pause. Lower it with `hold` and `release: true` in the tools, or `resume` in the CLI.
|
|
67
67
|
|
|
68
68
|
## Step out
|
|
69
69
|
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { type LedgerMaintenanceReport } from '../ledger-maintenance.js';
|
|
2
|
-
import { type HostLedgerReport } from '../host-ledger-migration.js';
|
|
3
|
-
import { type CommandSpec } from './context.js';
|
|
4
|
-
type LedgerIntent = {
|
|
5
|
-
readonly action: 'verify' | 'migrate';
|
|
6
|
-
} | {
|
|
7
|
-
readonly action: 'gc';
|
|
8
|
-
readonly dryRun: boolean;
|
|
9
|
-
};
|
|
10
|
-
type LedgerResult = HostLedgerReport | LedgerMaintenanceReport;
|
|
11
|
-
export declare const ledgerCommand: CommandSpec<LedgerIntent, LedgerResult>;
|
|
12
|
-
export {};
|
|
@@ -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 {};
|