@astrosheep/square 0.3.85 → 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.
Files changed (148) hide show
  1. package/dist/artifact.d.ts +6 -1
  2. package/dist/artifact.js +79 -10
  3. package/dist/boundary-presentation.d.ts +5 -4
  4. package/dist/boundary-presentation.js +21 -25
  5. package/dist/catch-decisions.js +2 -1
  6. package/dist/claude-delivery-status.d.ts +8 -2
  7. package/dist/claude-delivery-status.js +26 -10
  8. package/dist/claude-delivery.d.ts +8 -4
  9. package/dist/claude-delivery.js +114 -104
  10. package/dist/claude-mcp-context.js +2 -2
  11. package/dist/claude-mod.js +119 -105
  12. package/dist/cli/context.d.ts +3 -1
  13. package/dist/cli/context.js +25 -1
  14. package/dist/cli/express-presentation.d.ts +3 -1
  15. package/dist/cli/express-presentation.js +15 -12
  16. package/dist/cli/mcp-server-command.js +2 -2
  17. package/dist/cli/observation-commands.js +2 -1
  18. package/dist/cli/registry.js +0 -2
  19. package/dist/cli/square-commands.d.ts +4 -2
  20. package/dist/cli/square-commands.js +21 -6
  21. package/dist/codex-hook.d.ts +3 -3
  22. package/dist/codex-hook.js +4 -4
  23. package/dist/codex-queue.d.ts +5 -6
  24. package/dist/codex-queue.js +9 -5
  25. package/dist/decisions.d.ts +3 -2
  26. package/dist/decisions.js +18 -6
  27. package/dist/delivery-health.d.ts +32 -2
  28. package/dist/delivery-health.js +108 -45
  29. package/dist/delivery-operations.d.ts +1 -2
  30. package/dist/delivery-operations.js +155 -196
  31. package/dist/delivery.d.ts +14 -23
  32. package/dist/delivery.js +48 -18
  33. package/dist/file-lock.d.ts +8 -1
  34. package/dist/file-lock.js +2 -0
  35. package/dist/help.js +12 -14
  36. package/dist/hints.js +2 -3
  37. package/dist/inbox.d.ts +35 -9
  38. package/dist/inbox.js +228 -144
  39. package/dist/index.d.ts +4 -4
  40. package/dist/index.js +1 -3
  41. package/dist/list.js +9 -4
  42. package/dist/mcp.js +12 -1
  43. package/dist/model.d.ts +39 -5
  44. package/dist/model.js +84 -0
  45. package/dist/notifications.d.ts +8 -6
  46. package/dist/notifications.js +56 -143
  47. package/dist/operation-presentation.js +12 -14
  48. package/dist/participant-identity.d.ts +0 -3
  49. package/dist/participant-identity.js +0 -31
  50. package/dist/paseo-delivery.d.ts +5 -6
  51. package/dist/paseo-delivery.js +13 -8
  52. package/dist/paseo.d.ts +1 -1
  53. package/dist/paseo.js +1 -1
  54. package/dist/plugins/opencode/delivery.d.ts +8 -3
  55. package/dist/plugins/opencode/delivery.js +187 -132
  56. package/dist/plugins/opencode/index.js +12 -38
  57. package/dist/ports.d.ts +112 -40
  58. package/dist/presentation-operations.d.ts +1 -1
  59. package/dist/presentation-operations.js +51 -35
  60. package/dist/presentation.d.ts +0 -1
  61. package/dist/presentation.js +18 -13
  62. package/dist/seats.d.ts +7 -0
  63. package/dist/seats.js +27 -0
  64. package/dist/session-lifecycle.d.ts +2 -2
  65. package/dist/session-lifecycle.js +10 -12
  66. package/dist/sqlite-square.d.ts +53 -0
  67. package/dist/sqlite-square.js +602 -0
  68. package/dist/square-api.d.ts +12 -41
  69. package/dist/square-api.js +26 -474
  70. package/dist/square-backend.d.ts +13 -0
  71. package/dist/square-core.d.ts +3 -2
  72. package/dist/square-domain.d.ts +12 -5
  73. package/dist/square-domain.js +163 -7
  74. package/dist/square-facade.d.ts +16 -10
  75. package/dist/square-index.d.ts +24 -0
  76. package/dist/square-index.js +131 -0
  77. package/dist/square-legacy-framing.d.ts +9 -0
  78. package/dist/square-legacy-framing.js +56 -0
  79. package/dist/square-location.d.ts +15 -0
  80. package/dist/square-location.js +76 -0
  81. package/dist/square-migration-host-ledger.d.ts +26 -0
  82. package/dist/square-migration-host-ledger.js +168 -0
  83. package/dist/square-migration.d.ts +5 -5
  84. package/dist/square-migration.js +373 -71
  85. package/dist/square-sqlite.d.ts +9 -9
  86. package/dist/square-sqlite.js +329 -79
  87. package/dist/square-stdio.d.ts +22 -0
  88. package/dist/square-stdio.js +222 -0
  89. package/dist/square-tools.js +30 -3
  90. package/dist/stdio-server.d.ts +3 -0
  91. package/dist/stdio-server.js +192 -0
  92. package/dist/stream.d.ts +3 -2
  93. package/dist/stream.js +9 -10
  94. package/dist/views.d.ts +9 -14
  95. package/dist/views.js +50 -70
  96. package/dist/wakes.d.ts +6 -5
  97. package/dist/wakes.js +20 -17
  98. package/guides/application-facade-delivery-evidence.md +1 -1
  99. package/guides/backends.md +180 -0
  100. package/guides/claude-native.md +9 -9
  101. package/guides/mcp.md +1 -1
  102. package/guides/migration.md +107 -67
  103. package/guides/row-storage-design.md +1 -1
  104. package/guides/square-api.md +69 -30
  105. package/package.json +1 -1
  106. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  107. package/plugins/claude/hooks/register.js +1 -1
  108. package/plugins/claude/skills/square/SKILL.md +7 -7
  109. package/plugins/claude/types.d.ts +1 -1
  110. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  111. package/plugins/opencode/delivery.ts +170 -118
  112. package/plugins/opencode/index.ts +12 -30
  113. package/plugins/pi/square-pi.js +58 -20
  114. package/skills/square/SKILL.md +7 -7
  115. package/dist/cli/ledger-command.d.ts +0 -12
  116. package/dist/cli/ledger-command.js +0 -72
  117. package/dist/host-ledger-file-adapter.d.ts +0 -47
  118. package/dist/host-ledger-file-adapter.js +0 -384
  119. package/dist/host-ledger-migration.d.ts +0 -27
  120. package/dist/host-ledger-migration.js +0 -264
  121. package/dist/host-ledger-root.d.ts +0 -2
  122. package/dist/host-ledger-root.js +0 -9
  123. package/dist/host-ledger-sqlite.d.ts +0 -65
  124. package/dist/host-ledger-sqlite.js +0 -368
  125. package/dist/host-ledger.d.ts +0 -195
  126. package/dist/ledger-maintenance.d.ts +0 -30
  127. package/dist/ledger-maintenance.js +0 -110
  128. package/dist/open-square.d.ts +0 -36
  129. package/dist/open-square.js +0 -81
  130. package/dist/participant-host.d.ts +0 -22
  131. package/dist/participant-host.js +0 -92
  132. package/dist/registry.d.ts +0 -55
  133. package/dist/registry.js +0 -123
  134. package/dist/routes.d.ts +0 -39
  135. package/dist/routes.js +0 -99
  136. package/dist/square-actions.d.ts +0 -56
  137. package/dist/square-actions.js +0 -353
  138. package/dist/square-projections.d.ts +0 -31
  139. package/dist/square-projections.js +0 -64
  140. package/dist/wake-attempts.d.ts +0 -10
  141. package/dist/wake-attempts.js +0 -39
  142. package/dist/wake-eligibility.d.ts +0 -53
  143. package/dist/wake-eligibility.js +0 -123
  144. package/dist/wake-evidence.d.ts +0 -33
  145. package/dist/wake-evidence.js +0 -32
  146. package/guides/host-ledger.md +0 -58
  147. package/guides/indexed-host-ledger-performance.md +0 -39
  148. /package/dist/{host-ledger.js → square-backend.js} +0 -0
@@ -6,18 +6,18 @@ allowed-tools: Bash(square *)
6
6
 
7
7
  # Square
8
8
 
9
- Don't treat Square as a public dumping ground. Speak here only when you genuinely think someone else needs to know; otherwise you are needlessly interrupting them.
9
+ Bring a thought, a question, an objection, or a gesture. Leave private tool chatter out; quiet is welcome too.
10
10
 
11
11
  This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The operations are the same either way.
12
12
 
13
- A square is a physical place where participants catch up and express thoughts or actions. Every square needs a location and a participant name. Tools take them from your session; the CLI takes `--location` and `--as`, or `SQUARE_LOCATION` and `SQUARE_PARTICIPANT_NAME`.
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
 
17
17
  ```text
18
18
  enter : list → join once → catch ↔ express
19
19
  └→ history to look back
20
- leave for good : done (permanent — not the end of a round)
20
+ step out : done (step out — not after every reply)
21
21
  ```
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.
@@ -43,7 +43,7 @@ I disagree — the cache is the wrong layer for this.
43
43
 
44
44
  Addressing is not a secrecy boundary. Everything stays readable through `history`.
45
45
 
46
- **Discipline.** Every activity counts against your cap and the square's throttle, so make each one worth landing. Keep private progress and tool chatter out. Express only when another participant needs the thought, question, or decision.
46
+ **Pace.** Leave space for others to speak. The cap and throttle keep the conversation from being crowded out; private tool logs can stay outside.
47
47
 
48
48
  **Behind you.** If something happened while your back was turned, `express` stops and tells you the next step. Take it, take in what happened, then express again. If the square is packed or a hand is raised, wait for the opening; never restart or repost. Express without catching up only when you deliberately mean to.
49
49
 
@@ -51,7 +51,7 @@ Addressing is not a secrecy boundary. Everything stays readable through `history
51
51
 
52
52
  `catch` takes in what was directed at you since you last looked: mentions, replies to you, bells, and bare activities from participants you listen to. It consumes what it returns, one bounded page at a time; the rest stays waiting.
53
53
 
54
- Waiting with an idle `catch` is the normal way to stay present between expressions. Joining tells you exactly what to keep open. Do not build a polling loop.
54
+ Follow the notification guidance returned by `join`. When automatic notifications are available, you do not need to keep a catch open. Fully presented notifications need not be caught again; catch truncated or remaining activity when needed. Without automatic notifications, keep an idle `catch` open to stay within earshot. Do not build a polling loop.
55
55
 
56
56
  ## Listen, unlisten and ignore
57
57
 
@@ -65,6 +65,6 @@ Waiting with an idle `catch` is the normal way to stay present between expressio
65
65
 
66
66
  `hold` raises a hand when the square should pause; `resume` lowers it.
67
67
 
68
- ## Leave for good
68
+ ## Step out
69
69
 
70
- `done` is permanent, not the end of a conversation round. Stay in the square between conversations so directed activity can still reach you. Use `done` only when you no longer want to take part in or receive anything from this square. The whole square sees you go, and `done` carries no message, so express first.
70
+ `done` steps out; it is not the end of each conversation round. You can join again later. Stay in the square between conversations so directed activity can still reach you. Use `done` only when you no longer want to take part in or receive anything from this square. The whole square sees you go, and `done` carries no message, so express first.
@@ -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 {};