@astrosheep/square 0.3.72 → 0.3.74

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 (59) hide show
  1. package/dist/artifact.d.ts +0 -3
  2. package/dist/artifact.js +5 -59
  3. package/dist/boundary-presentation.d.ts +8 -1
  4. package/dist/boundary-presentation.js +8 -1
  5. package/dist/catch-decisions.d.ts +7 -1
  6. package/dist/catch-decisions.js +21 -8
  7. package/dist/claude-delivery.d.ts +1 -1
  8. package/dist/claude-delivery.js +7 -3
  9. package/dist/cli/context.d.ts +1 -1
  10. package/dist/cli/ledger-command.d.ts +12 -0
  11. package/dist/cli/ledger-command.js +68 -0
  12. package/dist/cli/registry.js +2 -0
  13. package/dist/help.js +7 -2
  14. package/dist/host-ledger-file-adapter.d.ts +4 -10
  15. package/dist/host-ledger-file-adapter.js +238 -198
  16. package/dist/host-ledger-migration.d.ts +26 -0
  17. package/dist/host-ledger-migration.js +227 -0
  18. package/dist/host-ledger-sqlite.d.ts +62 -0
  19. package/dist/host-ledger-sqlite.js +341 -0
  20. package/dist/host-ledger.d.ts +13 -4
  21. package/dist/inbox.d.ts +2 -0
  22. package/dist/inbox.js +64 -22
  23. package/dist/ledger-maintenance.d.ts +30 -0
  24. package/dist/ledger-maintenance.js +110 -0
  25. package/dist/mcp.d.ts +2 -1
  26. package/dist/mcp.js +51 -26
  27. package/dist/model.js +5 -2
  28. package/dist/open-square.d.ts +12 -2
  29. package/dist/open-square.js +42 -4
  30. package/dist/participant-host.js +6 -2
  31. package/dist/ports.d.ts +17 -1
  32. package/dist/runtime.d.ts +2 -1
  33. package/dist/runtime.js +12 -6
  34. package/dist/sqlite-transaction.d.ts +5 -0
  35. package/dist/sqlite-transaction.js +72 -0
  36. package/dist/square-actions.d.ts +3 -1
  37. package/dist/square-actions.js +26 -13
  38. package/dist/square-application.d.ts +4 -0
  39. package/dist/square-application.js +41 -27
  40. package/dist/square-facade.d.ts +26 -22
  41. package/dist/square-migration.js +2 -1
  42. package/dist/square-sqlite.d.ts +2 -2
  43. package/dist/square-sqlite.js +15 -4
  44. package/dist/views.d.ts +3 -1
  45. package/dist/views.js +37 -29
  46. package/guides/application-demand-optimization.md +50 -0
  47. package/guides/catch-cold-path-optimization.md +56 -0
  48. package/guides/catch-contention-performance.md +157 -0
  49. package/guides/catch-lock-investigation.md +81 -0
  50. package/guides/change-waits.md +1 -1
  51. package/guides/host-ledger.md +53 -6
  52. package/guides/indexed-host-ledger-performance.md +39 -0
  53. package/guides/mcp-connection-lifetime.md +22 -0
  54. package/guides/pi-boundary-pending-reuse.md +13 -0
  55. package/package.json +1 -1
  56. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  57. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  58. package/plugins/paseo/client/square-message.tsx +35 -19
  59. package/plugins/pi/square-pi.js +4 -8
@@ -22,9 +22,6 @@ export interface SquareSnapshot {
22
22
  }
23
23
  /** Private artifact-boundary connection shared with explicit migration. Never creates a path. */
24
24
  export declare function openExistingSquareDatabase(location: string): DatabaseSync;
25
- export declare function retrySqliteBusy<T>(operation: () => T, signal?: AbortSignal): Promise<T>;
26
- /** Acquisition may retry; after change starts, only COMMIT retries, retaining its result. */
27
- export declare function inSqliteTransaction<T>(db: DatabaseSync, write: boolean, validate: () => void, change: () => T, signal?: AbortSignal): Promise<T>;
28
25
  /** One connection, ordered operations, guarded synchronous transaction capabilities. */
29
26
  export declare function createArtifactPort(location: string, initial?: SquareState, externalSignal?: AbortSignal): SquareArtifactPort;
30
27
  /** New-format creation; initial authority is accepted only at this explicit boundary. */
package/dist/artifact.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
+ import { retrySqliteBusy, inSqliteTransaction } from './sqlite-transaction.js';
2
3
  import { randomUUID } from 'node:crypto';
3
4
  import fs from 'node:fs';
4
5
  import path from 'node:path';
@@ -57,9 +58,6 @@ function requireSquareExtension(location) {
57
58
  if (!location.endsWith('.square'))
58
59
  throw new SquareError('invalid_args', `Square artifacts must use the .square extension: ${location}`);
59
60
  }
60
- function isBusy(error) {
61
- return error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
62
- }
63
61
  function storageError(error, location) {
64
62
  if (!(error instanceof Error) || error instanceof SquareError || error instanceof InternalSquareError)
65
63
  return error;
@@ -85,69 +83,17 @@ function abort(signal) {
85
83
  if (signal?.aborted)
86
84
  throw signal.reason ?? new Error('Square artifact operation aborted');
87
85
  }
88
- export async function retrySqliteBusy(operation, signal) {
89
- while (true) {
90
- abort(signal);
91
- try {
92
- return operation();
93
- }
94
- catch (error) {
95
- if (!isBusy(error))
96
- throw error;
97
- try {
98
- await sleep(25, undefined, { signal });
99
- }
100
- catch (error) {
101
- abort(signal);
102
- throw error;
103
- }
104
- }
105
- }
106
- }
107
86
  function synchronous(value) {
108
87
  if (value !== null && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function') {
109
88
  void Promise.resolve(value).catch(() => undefined);
110
89
  throw new TypeError('Square artifact callbacks must be synchronous.');
111
90
  }
112
91
  }
113
- /** Acquisition may retry; after change starts, only COMMIT retries, retaining its result. */
114
- export async function inSqliteTransaction(db, write, validate, change, signal) {
115
- let begun = false;
116
- try {
117
- await retrySqliteBusy(() => {
118
- db.exec(write ? 'BEGIN IMMEDIATE' : 'BEGIN');
119
- begun = true;
120
- try {
121
- validate();
122
- }
123
- catch (error) {
124
- db.exec('ROLLBACK');
125
- begun = false;
126
- throw error;
127
- }
128
- }, signal);
129
- abort(signal);
130
- const result = change();
131
- synchronous(result);
132
- await retrySqliteBusy(() => db.exec('COMMIT'), signal);
133
- begun = false;
134
- return result;
135
- }
136
- catch (error) {
137
- if (begun) {
138
- try {
139
- db.exec('ROLLBACK');
140
- }
141
- catch { /* retain the original failure */ }
142
- }
143
- throw error;
144
- }
145
- }
146
92
  const readMethods = [
147
93
  'header', 'participant', 'roster', 'attention', 'incomingAttention', 'hold',
148
94
  'activity', 'activityHeads', 'activities', 'activityCount', 'contextIndexes', 'sayOrdinals', 'recentSayTimes',
149
95
  'audience', 'directed', 'directedAt', 'directedCount', 'pendingAudience',
150
- 'receipt', 'receiptFacts', 'receiptAssignments', 'lease', 'routes',
96
+ 'receipt', 'receiptFacts', 'receiptAssignments', 'lease', 'routes', 'participantCounts',
151
97
  ];
152
98
  const writeMethods = ['applyLanding', 'markSeen', 'putLease', 'removeLease', 'replaceRoutes'];
153
99
  /** One connection, ordered operations, guarded synchronous transaction capabilities. */
@@ -202,8 +148,8 @@ export function createArtifactPort(location, initial, externalSignal) {
202
148
  }
203
149
  async function transaction(callback, write, signal) {
204
150
  const db = await connection(signal);
205
- return inSqliteTransaction(db, write, () => validate(db), () => {
206
- const driver = createSqliteDriver(db), version = driver.header().revision;
151
+ return inSqliteTransaction(db, write, () => validate(db), (header) => {
152
+ const driver = createSqliteDriver(db), version = header.revision;
207
153
  let active = true, failed = false, failure;
208
154
  function guard(fn) {
209
155
  return ((...args) => {
@@ -250,7 +196,7 @@ export function createArtifactPort(location, initial, externalSignal) {
250
196
  }
251
197
  const revision = signal => queue(async (s) => {
252
198
  const db = await connection(s);
253
- return inSqliteTransaction(db, false, () => validate(db), () => createSqliteDriver(db).header().revision, s);
199
+ return inSqliteTransaction(db, false, () => validate(db), (header) => header.revision, s);
254
200
  }, signal);
255
201
  return {
256
202
  read: (select, signal) => queue(async (s) => {
@@ -1,5 +1,12 @@
1
1
  import type { InboxMembership } from './model.js';
2
2
  export { pendingAtBoundary } from './square-projections.js';
3
3
  export declare function renderPendingAtBoundary(inbox: InboxMembership[]): string;
4
- /** Serialize a boundary across hook processes so one stale inbox cannot render twice. */
4
+ /**
5
+ * Serialize a boundary across hook processes so one stale inbox cannot render twice.
6
+ *
7
+ * `lookup` returns the session's memberships for this boundary. It may reuse an observation it has
8
+ * verified against the current presence binding and artifact revision; the boundary still projects
9
+ * presentation evidence live, and `presentPending` still re-checks the pending head, join, ownership
10
+ * claim and seen CAS. A source that cannot verify its reuse must read the inbox again.
11
+ */
5
12
  export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
@@ -140,7 +140,14 @@ async function presentPendingAtBoundaryUnlocked(sessionId, present, lookup = ses
140
140
  }
141
141
  return result;
142
142
  }
143
- /** Serialize a boundary across hook processes so one stale inbox cannot render twice. */
143
+ /**
144
+ * Serialize a boundary across hook processes so one stale inbox cannot render twice.
145
+ *
146
+ * `lookup` returns the session's memberships for this boundary. It may reuse an observation it has
147
+ * verified against the current presence binding and artifact revision; the boundary still projects
148
+ * presentation evidence live, and `presentPending` still re-checks the pending head, join, ownership
149
+ * claim and seen CAS. A source that cannot verify its reuse must read the inbox again.
150
+ */
144
151
  export async function presentPendingAtBoundary(sessionId, present, lookup = sessionInbox, env = process.env, signal) {
145
152
  const ledgerRoot = hostLedgerRoot(env);
146
153
  const ownerKey = createHash('sha256').update(sessionId).digest('hex');
@@ -1,6 +1,6 @@
1
1
  import { type ActivityId } from './square-core.js';
2
2
  import { type StoredAct } from './model.js';
3
- import type { DirectedFilter, SquareRead, SquareWrite } from './ports.js';
3
+ import type { ActivityIndex, DirectedFilter, SquareRead, SquareWrite } from './ports.js';
4
4
  import type { CatchOptions } from './square-facade.js';
5
5
  export interface CatchSelection {
6
6
  readonly participant: string;
@@ -20,5 +20,11 @@ export declare function observeCatchSelection(facts: SquareRead, participant: st
20
20
  export declare const CATCH_DEFAULT_LIMIT = 10;
21
21
  export declare const CATCH_MAX_LIMIT = 100;
22
22
  export declare function catchLimit(value: number | undefined): number;
23
+ /** Select non-ID catch coordinates without hydrating bodies or recording receipts. */
24
+ export declare function selectCatchPage(facts: SquareRead, requestedName: string, options: CatchOptions): {
25
+ readonly participant: string;
26
+ readonly indexes: readonly ActivityIndex[];
27
+ readonly filter: DirectedFilter;
28
+ };
23
29
  /** Selection, receipts and resulting cursor belong to one artifact write transaction. */
24
30
  export declare function decideCatch(facts: SquareWrite, requestedName: string, options: CatchOptions, at: number): CatchSelection;
@@ -1,17 +1,20 @@
1
1
  import { formatActivityId, parseActivityId } from './square-core.js';
2
- import { inSquareCount, readCursor, watchTerminalStatus } from './runtime.js';
2
+ import { readCursor, terminalStatusFor } from './runtime.js';
3
3
  import { SquareError } from './model.js';
4
4
  import { resolveKnownName } from './decisions.js';
5
5
  /** Capture a selected page (or an empty completion) without opening another resource. */
6
6
  export function observeCatchSelection(facts, participant, activities, filter, at) {
7
+ if (typeof at !== 'number' || !Number.isFinite(at))
8
+ throw new SquareError('invalid_args', 'Catch time must be a finite number.');
7
9
  const cursor = readCursor(facts, participant);
10
+ const counts = facts.participantCounts(), header = facts.header(), current = facts.participant(participant);
8
11
  return {
9
12
  participant, activities, consumedThrough: cursor < 0 ? null : formatActivityId(cursor),
10
13
  remaining: facts.directedCount(participant, filter),
11
14
  sayNumbers: activities.length === 0 ? {} : facts.sayOrdinals(activities.map((item) => item.index)),
12
- standingCount: inSquareCount(facts), held: facts.hold().active, now: at,
13
- ownActivityCount: facts.participant(participant).sayCount, hardCap: facts.header().hardCap,
14
- terminalStatus: watchTerminalStatus(facts, participant),
15
+ standingCount: counts.joined, held: facts.hold().active, now: at,
16
+ ownActivityCount: current.sayCount, hardCap: header.hardCap,
17
+ terminalStatus: terminalStatusFor(current, header.hardCap, counts),
15
18
  };
16
19
  }
17
20
  export const CATCH_DEFAULT_LIMIT = 10;
@@ -23,13 +26,21 @@ export function catchLimit(value) {
23
26
  }
24
27
  return limit;
25
28
  }
29
+ /** Select non-ID catch coordinates without hydrating bodies or recording receipts. */
30
+ export function selectCatchPage(facts, requestedName, options) {
31
+ const participant = resolveKnownName(facts, requestedName);
32
+ const limit = catchLimit(options.limit);
33
+ const filter = { unseenOnly: true, ...(options.from === undefined ? {} : { senders: options.from }), ...(options.mention === true ? { attentionOnly: true } : {}) };
34
+ return { participant, indexes: facts.directed(participant, filter, { edge: 'oldest', limit }).items.map((item) => item.index), filter };
35
+ }
26
36
  /** Selection, receipts and resulting cursor belong to one artifact write transaction. */
27
37
  export function decideCatch(facts, requestedName, options, at) {
28
- const viewer = resolveKnownName(facts, requestedName);
29
- const limit = catchLimit(options.limit);
38
+ let viewer;
30
39
  let delivered;
31
40
  let filter = { unseenOnly: true };
32
41
  if (options.id !== undefined) {
42
+ viewer = resolveKnownName(facts, requestedName);
43
+ catchLimit(options.limit);
33
44
  if (['idle', 'from', 'mention', 'limit'].some((key) => Object.hasOwn(options, key)))
34
45
  throw new SquareError('invalid_args', 'Catch id cannot be combined with idle, from, mention or limit.');
35
46
  const index = parseActivityId(options.id);
@@ -44,8 +55,10 @@ export function decideCatch(facts, requestedName, options, at) {
44
55
  delivered = [activity];
45
56
  }
46
57
  else {
47
- filter = { unseenOnly: true, ...(options.from === undefined ? {} : { senders: options.from }), ...(options.mention === true ? { attentionOnly: true } : {}) };
48
- delivered = facts.activities(facts.directed(viewer, filter, { edge: 'oldest', limit }).items.map((item) => item.index));
58
+ const page = selectCatchPage(facts, requestedName, options);
59
+ viewer = page.participant;
60
+ filter = page.filter;
61
+ delivered = facts.activities(page.indexes);
49
62
  }
50
63
  facts.markSeen(viewer, delivered.map((item) => item.index), at);
51
64
  return observeCatchSelection(facts, viewer, delivered, filter, at);
@@ -3,7 +3,7 @@ import type { WakeTransportPort } from './ports.js';
3
3
  export declare function nativeTokens(text: string): string[];
4
4
  export declare function nativePayload(token: string, preview: string): string;
5
5
  export declare function createClaudeWakeTransport(hostLedger: HostLedgerPort, env?: NodeJS.ProcessEnv): WakeTransportPort;
6
- /** Authority comes only from bound memberships and the existing attempt ledger. */
6
+ /** Retired tokens veto a frame; only live correlations and bound memberships confer authority. */
7
7
  export declare function observeClaudeDelivery(sessionId: string, text: string, operation: 'guard' | 'admitted' | 'stored', env?: NodeJS.ProcessEnv): Promise<{
8
8
  recognized: boolean;
9
9
  current: boolean;
@@ -101,15 +101,19 @@ async function dispatchClaude(request, hostLedger, timeoutMs, beforeSend, env =
101
101
  return writeClaudeWake(row.session, native.endpoint, payload, timeoutMs, signal);
102
102
  }, signal);
103
103
  }
104
- /** Authority comes only from bound memberships and the existing attempt ledger. */
104
+ /** Retired tokens veto a frame; only live correlations and bound memberships confer authority. */
105
105
  export async function observeClaudeDelivery(sessionId, text, operation, env = process.env) {
106
106
  const ledger = hostLedgerForEnv(env);
107
107
  return ledger.withClaimLock(async () => {
108
108
  const tokens = nativeTokens(text);
109
- const rows = (await ledger.listEvidence({ kind: 'wake', session: sessionId })).filter((row) => row.nativeDelivery?.harness === 'claude' && row.claimToken && tokens.includes(row.claimToken) && ['dispatching', 'unknown', 'accepted'].includes(row.outcome));
109
+ const { deliveries: rows, retired } = await ledger.lookupNativeTokens({ harness: 'claude', session: sessionId, tokens });
110
+ // A stale token vetoes the entire frame, including a mixed current/stale frame,
111
+ // before admission, artifact reads or presentation can confer any authority.
112
+ if (retired.length)
113
+ return { recognized: true, current: false };
110
114
  let current = false;
111
115
  for (const row of rows) {
112
- if (row.nativeDelivery?.harness !== 'claude')
116
+ if (row.nativeDelivery?.harness !== 'claude' || !['dispatching', 'unknown', 'accepted'].includes(row.outcome))
113
117
  continue;
114
118
  const native = row.nativeDelivery;
115
119
  const pending = await currentDelivery(row, ledger).catch(() => undefined);
@@ -33,5 +33,5 @@ export interface ParsedGlobalArgs {
33
33
  export declare function parseGlobalArgs(rawArgs: string[]): Promise<ParsedGlobalArgs>;
34
34
  export declare function locationIsRequired(command: string): boolean;
35
35
  export declare function defaultContext(command: string, squarePath?: string, name?: string): CommandContext;
36
- export declare function applicationFor(context: CommandContext): import("../square-facade.js").SquareApplication;
36
+ export declare function applicationFor(context: CommandContext): import("../square-facade.js").SquareApplication<true>;
37
37
  export declare function requireSquarePath(context: CommandContext): string;
@@ -0,0 +1,12 @@
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 {};
@@ -0,0 +1,68 @@
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
+ `presence: ${report.presence} · evidence: ${report.evidence}`,
8
+ ];
9
+ if (report.presenceRevision !== undefined) {
10
+ lines.push(`revisions: presence ${report.presenceRevision} · evidence ${report.evidenceRevision}`);
11
+ }
12
+ if (report.status === 'upgraded')
13
+ lines.push('SQLite schema upgraded in place; records, order and revisions preserved.');
14
+ if (report.ready !== undefined)
15
+ lines.push(`readiness: ${report.ready ? 'ready' : 'not ready'}`);
16
+ if (report.sources !== undefined) {
17
+ for (const kind of ['presence', 'evidence']) {
18
+ const source = report.sources[kind];
19
+ lines.push(`${kind}.ndjsonl: ${source.records} accepted · ${source.ignored} ignored · ${source.rejected} rejected · sha256 ${source.sha256 ?? 'absent'}`);
20
+ }
21
+ }
22
+ if (report.status === 'legacy') {
23
+ if (report.ready)
24
+ lines.push('Stop all Square hosts before conversion.', 'square ledger migrate');
25
+ else
26
+ lines.push('Unsafe legacy records; conversion refused.');
27
+ }
28
+ return lines.join('\n') + '\n';
29
+ }
30
+ /** Counts only: unreadable locations are summarized, never printed, so no payload or credential can leak. */
31
+ function renderMaintenance(report) {
32
+ return [
33
+ `host ledger: gc${report.dryRun ? ' (dry run)' : ''}`,
34
+ `eligible: ${report.eligible} · retired: ${report.retired} · cas misses: ${report.casMisses}`,
35
+ `unreadable artifacts: ${report.unavailableArtifacts.length}`,
36
+ ...(report.dryRun ? ['Dry run: nothing retired; ledger and artifacts unchanged.'] : []),
37
+ '',
38
+ ].join('\n');
39
+ }
40
+ export const ledgerCommand = {
41
+ parse(argv, context) {
42
+ if (argv[0] === 'gc') {
43
+ if (argv.length === 1)
44
+ return { action: 'gc', dryRun: false };
45
+ if (argv.length === 2 && argv[1] === '--dry-run')
46
+ return { action: 'gc', dryRun: true };
47
+ return usage(context.command);
48
+ }
49
+ if (argv.length !== 1 || (argv[0] !== 'verify' && argv[0] !== 'migrate'))
50
+ usage(context.command);
51
+ return { action: argv[0] };
52
+ },
53
+ execute(intent, context) {
54
+ if (intent.action === 'gc')
55
+ return gcHostLedger({ env: context.env, dryRun: intent.dryRun });
56
+ const options = { env: context.env };
57
+ return intent.action === 'verify' ? verifyHostLedger(options) : migrateHostLedger(options);
58
+ },
59
+ present(result) {
60
+ if ('dryRun' in result) {
61
+ process.stdout.write(renderMaintenance(result));
62
+ return;
63
+ }
64
+ process.stdout.write(renderReport(result));
65
+ if (result.ready === false)
66
+ process.exitCode = 2;
67
+ },
68
+ };
@@ -1,5 +1,6 @@
1
1
  import { buildCommand, doneCommand, expressCommand, holdCommand, ignoreCommand, joinCommand, listenCommand, listeningCommand, resumeCommand } from './square-commands.js';
2
2
  import { doctorCommand, migrateCommand } from './maintenance-commands.js';
3
+ import { ledgerCommand } from './ledger-command.js';
3
4
  import { harnessCommand, installCommand, uninstallCommand } from './harness-command.js';
4
5
  import { helpCommand, versionCommand } from './meta-commands.js';
5
6
  import { mcpServerCommand } from './mcp-server-command.js';
@@ -26,6 +27,7 @@ export const commandRegistry = [
26
27
  { names: ['harness'], spec: harnessCommand },
27
28
  { names: ['doctor'], spec: doctorCommand },
28
29
  { names: ['migrate'], spec: migrateCommand },
30
+ { names: ['ledger'], spec: ledgerCommand },
29
31
  { names: ['history'], spec: historyCommand },
30
32
  { names: ['status'], spec: statusCommand },
31
33
  { names: ['participants'], spec: participantsCommand },
package/dist/help.js CHANGED
@@ -77,7 +77,12 @@ const COMMANDS = [
77
77
  {
78
78
  names: ['migrate'], usage: 'migrate', usesSquare: true, group: 'maintenance',
79
79
  summary: 'Convert an existing version 1 square to row storage in place.',
80
- details: ['It is the only migration command: there is no --check, --backup or --force.', 'A version 2 square is left untouched; an unrelated or unsupported artifact is refused without a write.'],
80
+ details: ['For the selected square artifact only; there is no --check, --backup or --force.', 'A version 2 square is left untouched; an unrelated or unsupported artifact is refused without a write.'],
81
+ },
82
+ {
83
+ names: ['ledger'], usage: 'ledger <verify|migrate|gc>', usesSquare: false, group: 'maintenance',
84
+ summary: 'Verify, convert, or reclaim the machine-local host ledger.',
85
+ details: ['verify: read-only SQLite integrity or full legacy readiness with accepted/ignored/rejected counts and source hashes; not-ready exits 2, an empty root stays empty. An older or unsupported database is refused with the migrate command and is never upgraded by verify.', 'migrate: stop every Square host first, then convert legacy NDJSON, or upgrade an older SQLite ledger in place. Sources, records, order and observer revisions are preserved; an existing current ledger is an untouched no-op.', 'gc [--dry-run]: reclaim old wake/presentation evidence only where the square artifact proves the recipient is closed — the activity reached them and catch receipted it or they later rejoined. Dispatching, lifecycle and unprovable rows are retained. Deleted rows free reusable pages, not file size (no VACUUM); retired Claude recognition tokens keep growing in total.', 'Run gc with --dry-run first: it reports eligible, retired, cas-miss and unreadable-artifact counts without writing. Eligible rows are candidates only: a changed record or an unusable Claude correlation is retained.', 'No participant or square location is required. Uses SQUARE_HOST_LEDGER_ROOT, then SQUARE_REGISTRY’s directory, then ~/.square/host-ledger.', 'These commands never stop, restart, install into or schedule hosts; conversion and schema upgrade are never automatic. No --force or repair.'],
81
86
  },
82
87
  ];
83
88
  function definitionFor(command) {
@@ -90,7 +95,7 @@ export function renderGlobalHelp() {
90
95
  const groups = [
91
96
  { key: 'participant', title: 'In the square:', order: ['join', 'express', 'listen', 'ignore', 'listening', 'catch', 'history', 'status', 'hold', 'resume', 'done'] },
92
97
  { key: 'host', title: 'Prepare and manage:', order: ['build', 'list', 'participants'] },
93
- { key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor', 'migrate'] },
98
+ { key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor', 'migrate', 'ledger'] },
94
99
  ];
95
100
  const commandLines = groups.flatMap(({ key, title, order }) => [
96
101
  title,
@@ -1,33 +1,27 @@
1
1
  import { type VersionObserver } from './file-changes.js';
2
- import type { HostLedgerPort, PresenceRecord, PresenceKey, PresenceLookup, PresenceResult, PresenceClaimResult, EvidenceRecord, EvidenceClaim, EvidenceRelease, EvidenceLookup, EvidenceGc, ClaimResult, WakeAttemptClaim, WakeAttemptClaimInput, WakeAttemptLookup } from './host-ledger.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
3
  export interface HostLedgerFileAdapterOptions {
4
4
  rootPath?: string;
5
5
  now?: () => number;
6
6
  }
7
+ /** Existing port surface; each operation owns a short-lived SQLite connection. */
7
8
  export declare class FileHostLedgerPort implements HostLedgerPort {
8
9
  private readonly root;
9
10
  private readonly clock;
10
11
  constructor(o?: HostLedgerFileAdapterOptions);
11
- private file;
12
- /** Invalidation only: callers still use the ledger projections as their authority. */
13
12
  observeChanges(): Promise<{
14
13
  presence: VersionObserver<string>;
15
14
  evidence: VersionObserver<string>;
16
15
  }>;
17
- /** Time can expire bindings/evidence even when their files do not change. */
18
16
  nextExpiry(session: string): Promise<number>;
19
- /** The ownership claim critical section: the same lock as claimPresence and finalize. */
20
17
  withClaimLock<T>(fn: () => Promise<T>, signal?: AbortSignal): Promise<T>;
21
18
  claimPresence(i: PresenceRecord, signal?: AbortSignal): Promise<PresenceClaimResult>;
22
19
  ensurePresence(i: PresenceRecord): Promise<PresenceResult>;
23
- /** Local batch suppression is monotonic only for the captured host generation. */
24
20
  suppressPresence(i: PresenceRecord, through: number): Promise<boolean>;
25
21
  removePresence(i: PresenceKey): Promise<void>;
26
22
  removePresenceIfUnchanged(i: PresenceRecord): Promise<boolean>;
27
23
  listPresence(i?: PresenceLookup): Promise<readonly PresenceRecord[]>;
28
- /** One attempt row owns attention-wide exclusion and session-scoped history for a wake. */
29
24
  claimWakeAttempt(i: WakeAttemptClaimInput): Promise<WakeAttemptClaim>;
30
- /** One locked, token-fenced replacement; each operation owns its state condition. */
31
25
  private updateWake;
32
26
  listWakeAttempts(i?: WakeAttemptLookup): Promise<readonly EvidenceRecord[]>;
33
27
  prepareNativeWake(i: EvidenceRecord & {
@@ -40,11 +34,11 @@ export declare class FileHostLedgerPort implements HostLedgerPort {
40
34
  }): Promise<boolean>;
41
35
  claimEvidence(i: EvidenceClaim): Promise<ClaimResult>;
42
36
  releaseEvidence(i: EvidenceRelease): Promise<void>;
43
- /** Token-fenced terminal write. A token may only finish the attempt it claimed. */
44
37
  appendEvidence(i: EvidenceRecord & {
45
38
  readonly claimToken: string;
46
39
  }): Promise<void>;
47
40
  listEvidence(i?: EvidenceLookup): Promise<readonly EvidenceRecord[]>;
48
- gcEvidence(i: EvidenceGc): Promise<void>;
41
+ retireEvidenceIfUnchanged(expected: EvidenceRecord): Promise<boolean>;
42
+ lookupNativeTokens(input: NativeTokenLookup): Promise<NativeTokenResult>;
49
43
  }
50
44
  export declare function createHostLedgerPort(o?: HostLedgerFileAdapterOptions): FileHostLedgerPort;