@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
@@ -1,4 +1,4 @@
1
- import { SquareError, sameName } from './model.js';
1
+ import { SquareError, formatActivityId, parseActivityId, sameBody, sameName, validateBody, validateName } from './model.js';
2
2
  /** The single landing transition, shared by both adapters and explicit rebuild. */
3
3
  export function deriveLanding(activity, facts) {
4
4
  const audience = [];
@@ -64,18 +64,174 @@ export function landingFacts(read, activity) {
64
64
  replyAuthor: activity.kind === 'say' && activity.reply !== undefined ? read.activity(activity.reply)?.actor : undefined,
65
65
  };
66
66
  }
67
- /** Driver-owned methods validate rows/writes and track changes; the domain owns allocation. */
68
- export function wrapSquareWrite(read, writes) {
67
+ function validateGeneration(value) {
68
+ if (typeof value !== 'string' || parseActivityId(value) === undefined)
69
+ throw new SquareError('invalid_args', 'Artifact authority requires a valid join generation.');
70
+ }
71
+ /** The artifact transaction supplies one captured authority; every mutation checks it here. */
72
+ export function wrapSquareWrite(read, writes, authority) {
73
+ if (authority === undefined || typeof authority !== 'object' || authority === null)
74
+ throw new SquareError('invalid_args', 'Artifact writes require an explicit authority.');
69
75
  const { applyLanding, ...mutations } = writes;
70
- return Object.assign({}, read, mutations, {
76
+ const expectedName = authority.kind === 'participant' ? authority.seat?.participant : authority.participant;
77
+ if (!['participant', 'join', 'takeover'].includes(authority.kind) || typeof expectedName !== 'string')
78
+ throw new SquareError('invalid_args', 'Artifact write authority is malformed.');
79
+ validateName(expectedName);
80
+ if (authority.kind === 'participant') {
81
+ validateBody(authority.seat.body);
82
+ validateGeneration(authority.seat.generation);
83
+ }
84
+ else {
85
+ validateBody(authority.body);
86
+ if (authority.kind === 'takeover')
87
+ validateGeneration(authority.expectedGeneration);
88
+ }
89
+ const assertSeat = (seat, expected = authority.kind === 'participant' ? authority.seat : undefined) => {
90
+ if (expected === undefined || !sameName(expected.participant, seat.participant)
91
+ || seat.generation !== expected.generation || !sameBody(seat.body, expected.body))
92
+ throw new SquareError('already_joined', 'This write needs the current participant seat.');
93
+ const current = read.seat(seat.participant);
94
+ if (current === undefined || !sameBody(current.body, expected.body) || current.generation !== expected.generation)
95
+ throw new SquareError('already_joined', 'This participant seat has been replaced.');
96
+ };
97
+ const assertParticipant = (name, generation) => {
98
+ if (authority.kind !== 'participant')
99
+ throw new SquareError('already_joined', 'This write needs the current participant seat.');
100
+ assertSeat({ ...authority.seat, participant: name, generation: generation === undefined ? authority.seat.generation : generation });
101
+ };
102
+ let reconnectSeat;
103
+ let takeoverPhase = authority.kind === 'takeover' ? 'ready' : undefined;
104
+ if (authority.kind === 'participant') {
105
+ assertSeat(authority.seat);
106
+ }
107
+ else if (authority.kind === 'join') {
108
+ const participant = read.participant(expectedName), seat = read.seat(expectedName);
109
+ if (participant?.joined) {
110
+ if (seat === undefined || !sameBody(seat.body, authority.body))
111
+ throw new SquareError('already_joined', `${expectedName} is held by another body; take it over explicitly`);
112
+ reconnectSeat = seat;
113
+ }
114
+ }
115
+ else {
116
+ const current = read.seat(expectedName);
117
+ if (current === undefined || current.generation !== authority.expectedGeneration)
118
+ throw new SquareError('already_joined', 'The participant seat changed before takeover.');
119
+ }
120
+ let lastActivity;
121
+ const facts = Object.assign({}, read, {
71
122
  land(activity) {
123
+ const actor = 'actor' in activity ? activity.actor : undefined;
124
+ if (actor === undefined)
125
+ throw new SquareError('invalid_args', 'Participant activities require a named actor.');
126
+ validateName(actor);
127
+ if (!sameName(actor, expectedName))
128
+ throw new SquareError('already_joined', 'Activity actor does not match the artifact authority.');
129
+ if (authority.kind === 'participant') {
130
+ assertSeat(authority.seat);
131
+ if (activity.kind === 'join')
132
+ throw new SquareError('invalid_args', 'A participant seat can only be re-established by join.');
133
+ }
134
+ else if (authority.kind === 'join') {
135
+ if (activity.kind !== 'join')
136
+ throw new SquareError('invalid_args', 'A new seat must be established by its join activity.');
137
+ if (read.participant(expectedName)?.joined)
138
+ throw new SquareError('already_joined', `${expectedName} is already standing; join cannot replace an existing seat.`);
139
+ }
140
+ else if (takeoverPhase === 'ready') {
141
+ const current = read.seat(expectedName);
142
+ if (activity.kind !== 'done' || current === undefined || current.generation !== authority.expectedGeneration)
143
+ throw new SquareError('already_joined', 'Takeover must first complete the captured participant.');
144
+ }
145
+ else if (authority.kind === 'takeover' && takeoverPhase === 'done') {
146
+ if (activity.kind !== 'join' || !read.participant(expectedName)?.done)
147
+ throw new SquareError('already_joined', 'Takeover must join after its captured done activity.');
148
+ }
149
+ else {
150
+ throw new SquareError('invalid_args', 'Takeover permits only one done followed by one join.');
151
+ }
72
152
  const index = read.header().nextActIndex;
73
- if (!Number.isSafeInteger(index) || index < 0 || index >= Number.MAX_SAFE_INTEGER) {
153
+ if (!Number.isSafeInteger(index) || index < 0 || index >= Number.MAX_SAFE_INTEGER)
74
154
  throw new SquareError('invalid_args', 'Square activity index is exhausted.');
75
- }
155
+ const captured = authority.kind === 'takeover' && activity.kind === 'done'
156
+ ? read.seat(expectedName) : undefined;
76
157
  const stored = { ...activity, index };
77
158
  applyLanding(deriveLanding(stored, landingFacts(read, stored)));
78
- return read.activity(index);
159
+ if (stored.kind === 'join') {
160
+ if (authority.kind === 'participant')
161
+ throw new SquareError('invalid_args', 'Participant authority cannot establish a seat.');
162
+ const seat = {
163
+ participant: stored.actor,
164
+ body: authority.body,
165
+ generation: formatActivityId(stored.index),
166
+ ...(authority.wakeAddress === undefined ? {} : { wakeAddress: authority.wakeAddress }),
167
+ };
168
+ mutations.storeSeat(seat);
169
+ const established = read.seat(seat.participant);
170
+ if (established === undefined || !sameBody(established.body, seat.body) || established.generation !== seat.generation)
171
+ throw new SquareError('invalid_args', 'The join activity did not establish its artifact seat.');
172
+ if (authority.kind === 'takeover')
173
+ takeoverPhase = 'joined';
174
+ }
175
+ else if (stored.kind === 'done' && authority.kind === 'participant') {
176
+ if (!mutations.removeSeat(authority.seat))
177
+ throw new SquareError('already_joined', 'The participant seat changed while it was being completed.');
178
+ }
179
+ else if (stored.kind === 'done' && authority.kind === 'takeover') {
180
+ if (captured === undefined || !mutations.removeSeat({ participant: expectedName, body: captured.body, generation: captured.generation }))
181
+ throw new SquareError('already_joined', 'The participant seat changed while takeover was completing it.');
182
+ takeoverPhase = 'done';
183
+ }
184
+ lastActivity = read.activity(index);
185
+ return lastActivity;
186
+ },
187
+ updateWakeAddress(seat, wakeAddress) {
188
+ assertSeat(seat, authority.kind === 'join' ? reconnectSeat : undefined);
189
+ return mutations.updateWakeAddress(seat, wakeAddress);
190
+ },
191
+ cancelThrough(seat, through) {
192
+ assertSeat(seat);
193
+ if (!Number.isSafeInteger(through) || through < 0 || through > read.header().lastActIndex || read.activity(through) === undefined)
194
+ throw new SquareError('invalid_args', 'Cancellation boundary must name an existing activity at or below the current head.');
195
+ return mutations.cancelThrough(seat, through);
196
+ },
197
+ markSeen(name, indexes, at) {
198
+ assertParticipant(name);
199
+ return mutations.markSeen(name, indexes, at);
200
+ },
201
+ putLease(name, lease, generation) {
202
+ assertParticipant(name, generation);
203
+ return mutations.putLease(name, lease, generation);
204
+ },
205
+ removeLease(name, leaseId, generation) {
206
+ assertParticipant(name, generation);
207
+ return mutations.removeLease(name, leaseId, generation);
208
+ },
209
+ claimDelivery(input) {
210
+ assertSeat(input.seat);
211
+ return mutations.claimDelivery(input);
212
+ },
213
+ prepareDelivery(input) {
214
+ assertSeat(input.seat);
215
+ return mutations.prepareDelivery(input);
216
+ },
217
+ completeDelivery(input) {
218
+ assertSeat(input.seat);
219
+ return mutations.completeDelivery(input);
220
+ },
221
+ observeNativeDelivery(input) {
222
+ assertSeat(input.seat);
223
+ return mutations.observeNativeDelivery(input);
79
224
  },
80
225
  });
226
+ return {
227
+ facts,
228
+ complete() {
229
+ if (authority.kind !== 'takeover')
230
+ return;
231
+ const seat = read.seat(expectedName);
232
+ if (takeoverPhase !== 'joined' || lastActivity?.kind !== 'join' || seat === undefined
233
+ || !sameBody(seat.body, authority.body) || seat.generation !== formatActivityId(lastActivity.index))
234
+ throw new SquareError('already_joined', 'Takeover did not complete its replacement join.');
235
+ },
236
+ };
81
237
  }
@@ -1,7 +1,9 @@
1
+ import type { SquareBackends } from './square-backend.js';
1
2
  import type { ActivityId, Perception, Reach } from './square-core.js';
2
- import type { StoredAct } from './model.js';
3
- import type { HostLedgerPort, WakeTransportPort, DeliveryResult, SquareResourceOwner } from './ports.js';
3
+ import type { Body, StoredAct, WakeAddress, ActivityMetadata } from './model.js';
4
+ import type { WakeTransportPort, DeliveryResult, SquareResourceOwner } from './ports.js';
4
5
  export type { ActivityId, Perception, Reach } from './square-core.js';
6
+ export type { Body, WakeAddress, ActivityMetadata, JsonValue } from './model.js';
5
7
  /** Indexed archive event, including read and historical actorless lifecycle records. */
6
8
  export type ArchiveRecord = Readonly<StoredAct>;
7
9
  export interface Activity {
@@ -10,6 +12,7 @@ export interface Activity {
10
12
  readonly kind: 'join' | 'say' | 'done' | 'hold' | 'resume' | 'listen' | 'ignore' | 'unlisten';
11
13
  readonly actor: string;
12
14
  readonly body?: string;
15
+ readonly metadata?: ActivityMetadata;
13
16
  readonly mentions: readonly string[];
14
17
  readonly reach?: Reach;
15
18
  readonly target?: string;
@@ -23,6 +26,7 @@ export interface ObservationOptions {
23
26
  readonly observe?: boolean;
24
27
  }
25
28
  export interface ExpressOptions extends ObservationOptions {
29
+ readonly metadata?: ActivityMetadata;
26
30
  readonly force?: boolean;
27
31
  readonly mentions?: readonly string[];
28
32
  readonly reach?: Reach;
@@ -30,6 +34,9 @@ export interface ExpressOptions extends ObservationOptions {
30
34
  readonly noMention?: boolean;
31
35
  readonly noWait?: boolean;
32
36
  }
37
+ export interface ExpressInput extends ExpressOptions {
38
+ readonly body?: string;
39
+ }
33
40
  export interface ActivityResult {
34
41
  readonly activity: Activity;
35
42
  readonly delivery?: DeliveryResult;
@@ -146,9 +153,11 @@ export interface SquareSnapshot {
146
153
  }
147
154
  export interface OpenOptions {
148
155
  readonly clock?: () => number;
149
- readonly hostLedger?: HostLedgerPort;
150
156
  readonly wakeTransport?: WakeTransportPort;
151
- readonly env?: NodeJS.ProcessEnv;
157
+ /** Explicit execution coordinate resolved by an adapter. */
158
+ readonly body?: Body;
159
+ /** Optional wake address attached to this body. */
160
+ readonly wakeAddress?: WakeAddress;
152
161
  }
153
162
  export interface SquareLocation {
154
163
  readonly path?: string;
@@ -156,6 +165,7 @@ export interface SquareLocation {
156
165
  readonly env?: NodeJS.ProcessEnv;
157
166
  }
158
167
  export interface SquareAtInput extends SquareLocation, OpenOptions {
168
+ readonly backends?: SquareBackends;
159
169
  readonly participant?: string;
160
170
  /** A caller-owned connection; closing this Square never closes its owner. */
161
171
  readonly resources?: SquareResourceOwner;
@@ -179,13 +189,10 @@ export interface JoinResult {
179
189
  readonly participantCount: number;
180
190
  readonly observation?: JoinObservation;
181
191
  }
182
- /** Optional fence for owner-scoped lifecycle actions. */
183
- export interface OwnershipFenceOptions {
184
- readonly expectedEpoch?: number;
185
- }
186
192
  export interface Participant {
187
193
  readonly name: string;
188
- express(body: string, options?: ExpressOptions, control?: OperationControl): Promise<ExpressResult>;
194
+ express(body: string | undefined, options?: ExpressOptions, control?: OperationControl): Promise<ExpressResult>;
195
+ express(input: ExpressInput, control?: OperationControl): Promise<ExpressResult>;
189
196
  listen(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
190
197
  ignore(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
191
198
  unlisten(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
@@ -354,7 +361,6 @@ export interface ExpressObservation extends SquareObservation, ExpressionObserva
354
361
  readonly pendingPublic: readonly ArchiveRecord[];
355
362
  readonly sayNumbers: Readonly<Record<number, number>>;
356
363
  readonly perceptions: Readonly<Record<number, Perception>>;
357
- readonly reentry?: Activity;
358
364
  }
359
365
  export interface CatchObservation extends SquareObservation, ExpressionObservation {
360
366
  readonly participant: string;
@@ -0,0 +1,24 @@
1
+ import { type VersionObserver } from './file-changes.js';
2
+ import { type Body } from './model.js';
3
+ /** A rebuildable discovery hint: the body that may have used an artifact and its path.
4
+ * It is deliberately not a participant, generation, ownership, suppression, or evidence row. */
5
+ export interface SquareIndexEntry {
6
+ readonly location: string;
7
+ readonly body: Body;
8
+ }
9
+ export interface SquareIndexQuery {
10
+ readonly body?: Body;
11
+ readonly channel?: string;
12
+ readonly session?: string;
13
+ readonly agent?: string;
14
+ }
15
+ /** The index is the only host-side Square state. Its path is test/configuration plumbing, not authority. */
16
+ export declare function squareIndexPath(env?: NodeJS.ProcessEnv): string;
17
+ /** Read and verify the disposable index. Paths are canonicalized on access and duplicates collapse. */
18
+ export declare function indexedSquareEntries(query?: SquareIndexQuery, env?: NodeJS.ProcessEnv): Promise<readonly SquareIndexEntry[]>;
19
+ export declare function indexedSquarePaths(query?: SquareIndexQuery, env?: NodeJS.ProcessEnv): Promise<readonly string[]>;
20
+ /** Best-effort discovery only: a busy index skips publication, never queues or delays artifact work. */
21
+ export declare function rememberSquarePath(location: string, body: Body, index: string): Promise<boolean>;
22
+ /** Index loss is tolerated by every caller; this observer is only a wake hint for rediscovery. */
23
+ export declare function observeSquareIndex(env?: NodeJS.ProcessEnv): Promise<VersionObserver<string>>;
24
+ export declare function bodyKey(body: Body): string;
@@ -0,0 +1,131 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { withFileLock } from './file-lock.js';
6
+ import { canonicalPath } from './canonical-path.js';
7
+ import { observeFileMetadata } from './file-changes.js';
8
+ import { sameBody, validateBody } from './model.js';
9
+ /** The index is the only host-side Square state. Its path is test/configuration plumbing, not authority. */
10
+ export function squareIndexPath(env = process.env) {
11
+ const configured = env.SQUARE_INDEX?.trim();
12
+ return path.resolve(configured ?? path.join(os.homedir(), '.square', 'index.ndjsonl'));
13
+ }
14
+ function lockPath(index) {
15
+ return `${index}.lock`;
16
+ }
17
+ function validEntry(value) {
18
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
19
+ return undefined;
20
+ const row = value;
21
+ if (typeof row.location !== 'string' || !path.isAbsolute(row.location)
22
+ || row.body === null || typeof row.body !== 'object' || Array.isArray(row.body))
23
+ return undefined;
24
+ const raw = row.body;
25
+ const body = {
26
+ channel: raw.channel,
27
+ session: raw.session,
28
+ ...(raw.agent === undefined ? {} : { agent: raw.agent }),
29
+ };
30
+ try {
31
+ validateBody(body);
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ if (Object.keys(raw).some((key) => !['channel', 'session', 'agent'].includes(key)))
37
+ return undefined;
38
+ return { location: row.location, body };
39
+ }
40
+ async function readEntries(index) {
41
+ let content;
42
+ try {
43
+ content = await fs.readFile(index, 'utf8');
44
+ }
45
+ catch (error) {
46
+ if (error.code === 'ENOENT')
47
+ return [];
48
+ throw error;
49
+ }
50
+ const entries = [];
51
+ for (const line of content.split('\n')) {
52
+ if (line.trim() === '')
53
+ continue;
54
+ try {
55
+ const entry = validEntry(JSON.parse(line));
56
+ if (entry !== undefined)
57
+ entries.push(entry);
58
+ }
59
+ catch {
60
+ // The index is disposable discovery input. A malformed line is ignored rather than
61
+ // becoming a second authority or preventing access to valid entries beside it.
62
+ }
63
+ }
64
+ return entries;
65
+ }
66
+ function matches(entry, query) {
67
+ if (query.body !== undefined && !sameBody(entry.body, query.body))
68
+ return false;
69
+ if (query.channel !== undefined && entry.body.channel !== query.channel)
70
+ return false;
71
+ if (query.session !== undefined && entry.body.session !== query.session)
72
+ return false;
73
+ if (query.agent !== undefined && entry.body.agent !== query.agent)
74
+ return false;
75
+ return true;
76
+ }
77
+ /** Read and verify the disposable index. Paths are canonicalized on access and duplicates collapse. */
78
+ export async function indexedSquareEntries(query = {}, env = process.env) {
79
+ const entries = await readEntries(squareIndexPath(env));
80
+ const result = [];
81
+ const seen = new Set();
82
+ for (const entry of entries) {
83
+ if (!matches(entry, query))
84
+ continue;
85
+ let location;
86
+ try {
87
+ location = await canonicalPath(entry.location);
88
+ }
89
+ catch {
90
+ continue;
91
+ }
92
+ const key = JSON.stringify([location, entry.body.channel, entry.body.session, entry.body.agent ?? null]);
93
+ if (seen.has(key))
94
+ continue;
95
+ seen.add(key);
96
+ result.push({ location, body: entry.body });
97
+ }
98
+ return result;
99
+ }
100
+ export async function indexedSquarePaths(query = {}, env = process.env) {
101
+ return [...new Set((await indexedSquareEntries(query, env)).map((entry) => entry.location))];
102
+ }
103
+ /** Best-effort discovery only: a busy index skips publication, never queues or delays artifact work. */
104
+ export async function rememberSquarePath(location, body, index) {
105
+ validateBody(body);
106
+ const canonical = await canonicalPath(location);
107
+ await fs.mkdir(path.dirname(index), { recursive: true });
108
+ return await withFileLock(lockPath(index), { retryMs: 10, skipIfBusy: true }, async () => {
109
+ const entries = await readEntries(index);
110
+ if (entries.some((entry) => entry.location === canonical && sameBody(entry.body, body)))
111
+ return false;
112
+ const next = [...entries, { location: canonical, body }];
113
+ const temporary = `${index}.${process.pid}.${randomUUID()}.tmp`;
114
+ try {
115
+ await fs.writeFile(temporary, next.map((entry) => JSON.stringify(entry)).join('\n') + '\n', { flag: 'wx', mode: 0o600 });
116
+ await fs.rename(temporary, index);
117
+ }
118
+ finally {
119
+ await fs.rm(temporary, { force: true });
120
+ }
121
+ return true;
122
+ }) ?? false;
123
+ }
124
+ /** Index loss is tolerated by every caller; this observer is only a wake hint for rediscovery. */
125
+ export function observeSquareIndex(env = process.env) {
126
+ return observeFileMetadata(squareIndexPath(env));
127
+ }
128
+ export function bodyKey(body) {
129
+ validateBody(body);
130
+ return JSON.stringify([body.channel, body.session, body.agent ?? null]);
131
+ }
@@ -0,0 +1,9 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ export declare const FORMAT4_ADDED_OBJECTS: ReadonlySet<string>;
3
+ export declare const FORMAT4_REPLACED_OBJECTS: ReadonlySet<string>;
4
+ /**
5
+ * Read-only recognition of the exact row framing accepted by explicit migration.
6
+ * It inspects identity, owned schema and journal mode only; it never reads row
7
+ * contents (including legacy routes), changes pragmas, or performs DDL.
8
+ */
9
+ export declare function legacyRowFramingIssue(database: DatabaseSync, applicationId: number, version: 2 | 3, location: string, currentSchemaSql: string): string | undefined;
@@ -0,0 +1,56 @@
1
+ export const FORMAT4_ADDED_OBJECTS = new Set([
2
+ 'seats', 'seat_body', 'delivery_evidence', 'delivery_claim_token', 'delivery_activity', 'delivery_cancellations',
3
+ ]);
4
+ export const FORMAT4_REPLACED_OBJECTS = new Set(['routes', 'route_participant', 'route_session']);
5
+ const LEGACY_ROUTES_SQL = `CREATE TABLE routes (
6
+ position INTEGER PRIMARY KEY CHECK (position >= 0),
7
+ participant_key_json TEXT COLLATE BINARY NOT NULL,
8
+ session_json TEXT COLLATE BINARY NOT NULL,
9
+ kind TEXT NOT NULL CHECK (kind IN ('opencode-server','codex-queue','claude-native','paseo')),
10
+ updated_at REAL NOT NULL,
11
+ payload_json TEXT NOT NULL CHECK (json_valid(payload_json))
12
+ );
13
+ CREATE INDEX route_participant ON routes(participant_key_json, position);
14
+ CREATE INDEX route_session ON routes(session_json, position);`;
15
+ function normalizedSql(sql) {
16
+ return sql.trim().replace(/;$/, '').replace(/\s+/g, ' ').replace(/\s*([(),=])\s*/g, '$1');
17
+ }
18
+ function legacyRowSchema(currentSchemaSql, version) {
19
+ const statements = currentSchemaSql.split(';').map((statement) => statement.trim()).filter(Boolean)
20
+ .filter((statement) => {
21
+ const match = /^(?:CREATE (?:UNIQUE )?TABLE|CREATE (?:UNIQUE )?INDEX)\s+([a-z_]+)/i.exec(statement);
22
+ return match === null || !FORMAT4_ADDED_OBJECTS.has(match[1]) && !FORMAT4_REPLACED_OBJECTS.has(match[1]);
23
+ });
24
+ const legacyStatements = statements.join(';\n')
25
+ .replace(' delivery_revision INTEGER NOT NULL CHECK (delivery_revision BETWEEN 0 AND 9007199254740991),\n', '')
26
+ .replace(' generation_json TEXT NOT NULL DEFAULT \'""\',\n', '');
27
+ const allStatements = [...legacyStatements.split(';').map((statement) => statement.trim()).filter(Boolean),
28
+ ...LEGACY_ROUTES_SQL.split(';').map((statement) => statement.trim()).filter(Boolean)];
29
+ let schema = allStatements.join(';\n');
30
+ if (version === 2) {
31
+ schema = schema.replace("kind IN ('join','done','say','hold','resume','read','listen','ignore','unlisten')", "kind IN ('join','done','say','hold','resume','read','listen','ignore')");
32
+ }
33
+ return schema.split(';').map((statement) => normalizedSql(statement)).filter(Boolean).sort();
34
+ }
35
+ /**
36
+ * Read-only recognition of the exact row framing accepted by explicit migration.
37
+ * It inspects identity, owned schema and journal mode only; it never reads row
38
+ * contents (including legacy routes), changes pragmas, or performs DDL.
39
+ */
40
+ export function legacyRowFramingIssue(database, applicationId, version, location, currentSchemaSql) {
41
+ const identity = database.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
42
+ if (identity.application_id !== applicationId)
43
+ return 'unrelated SQLite database.';
44
+ if (identity.user_version !== version)
45
+ return 'unsupported SQLite format version.';
46
+ const actual = database.prepare("SELECT sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' AND sql IS NOT NULL").all()
47
+ .map((row) => typeof row.sql === 'string' ? normalizedSql(row.sql) : '')
48
+ .sort();
49
+ const expected = legacyRowSchema(currentSchemaSql, version);
50
+ if (actual.length !== expected.length || actual.some((sql, index) => sql !== expected[index]))
51
+ return 'unsupported row schema.';
52
+ const journalMode = database.prepare('PRAGMA journal_mode').get().journal_mode;
53
+ if (journalMode !== (location === ':memory:' ? 'memory' : 'delete'))
54
+ return 'unsupported journal mode.';
55
+ return undefined;
56
+ }
@@ -0,0 +1,15 @@
1
+ import type { SquareAtInput, SquareLocation } from './square-facade.js';
2
+ import type { SquareBackends } from './square-backend.js';
3
+ export type ParsedSquareLocation = {
4
+ readonly kind: 'local';
5
+ readonly path: string;
6
+ } | {
7
+ readonly kind: 'stdio';
8
+ readonly backend: string;
9
+ readonly key: string;
10
+ readonly uri: string;
11
+ };
12
+ export declare function parseSquareLocation(value: string): ParsedSquareLocation;
13
+ export declare function squareLocation(input: SquareLocation): string;
14
+ export declare function localSquareLocation(input: SquareLocation): string;
15
+ export declare function backendConfiguration(input: SquareAtInput): Promise<SquareBackends>;
@@ -0,0 +1,76 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { SquareError } from './model.js';
5
+ export function parseSquareLocation(value) {
6
+ if (!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value))
7
+ return { kind: 'local', path: value };
8
+ // Parse the raw components, before URL dot-segment normalization or host lowercasing.
9
+ const match = /^stdio:\/\/([A-Za-z0-9_-]+)\/([^/?#]+)$/.exec(value);
10
+ if (!match)
11
+ throw new SquareError('invalid_args', 'Expected a local path or stdio://<configured-name>/<encoded-square-key>; no credentials, port, query or fragment');
12
+ let key;
13
+ try {
14
+ key = decodeURIComponent(match[2]);
15
+ }
16
+ catch {
17
+ throw new SquareError('invalid_args', 'Invalid percent encoding in stdio square key');
18
+ }
19
+ if (!key || key === '.' || key === '..' || /[/\\\x00-\x1f\x7f]/.test(key))
20
+ throw new SquareError('invalid_args', 'A stdio square key must be one nonempty name, not a path');
21
+ return { kind: 'stdio', backend: match[1], key, uri: `stdio://${match[1]}/${encodeURIComponent(key)}` };
22
+ }
23
+ export function squareLocation(input) {
24
+ const selected = input.path ?? (input.env ?? process.env).SQUARE_LOCATION;
25
+ if (selected === undefined || selected.trim() === '')
26
+ throw new SquareError('invalid_args', 'Square needs a location');
27
+ const location = parseSquareLocation(selected);
28
+ return location.kind === 'local' ? path.resolve(input.cwd ?? process.cwd(), location.path) : location.uri;
29
+ }
30
+ export function localSquareLocation(input) {
31
+ const location = squareLocation(input);
32
+ if (parseSquareLocation(location).kind !== 'local')
33
+ throw new SquareError('invalid_args', 'This file operation requires a local square artifact; backend URIs are not supported');
34
+ return location;
35
+ }
36
+ function validateBackends(value) {
37
+ const object = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
38
+ const invalid = () => new SquareError('invalid_args', 'Invalid backend configuration: expected {backends:{name:{command,args?,cwd?,env?}}}');
39
+ if (!object(value))
40
+ throw invalid();
41
+ for (const [name, entry] of Object.entries(value)) {
42
+ if (!/^[A-Za-z0-9_-]+$/.test(name) || !object(entry) || typeof entry.command !== 'string' || !entry.command.trim()
43
+ || Object.keys(entry).some(key => !['command', 'args', 'cwd', 'env'].includes(key))
44
+ || (entry.args !== undefined && (!Array.isArray(entry.args) || entry.args.some(arg => typeof arg !== 'string')))
45
+ || (entry.cwd !== undefined && typeof entry.cwd !== 'string')
46
+ || (entry.env !== undefined && (!object(entry.env) || Object.values(entry.env).some(v => typeof v !== 'string'))))
47
+ throw invalid();
48
+ }
49
+ return value;
50
+ }
51
+ export async function backendConfiguration(input) {
52
+ if (input.backends !== undefined)
53
+ return validateBackends(input.backends);
54
+ const env = input.env ?? process.env;
55
+ const configured = env.SQUARE_BACKENDS_CONFIG;
56
+ const file = configured === undefined ? path.join(env.HOME ?? os.homedir(), '.config/square/backends.json')
57
+ : path.resolve(input.cwd ?? process.cwd(), configured);
58
+ let text;
59
+ try {
60
+ text = await readFile(file, 'utf8');
61
+ }
62
+ catch (error) {
63
+ if (configured === undefined && error.code === 'ENOENT')
64
+ return {};
65
+ throw new SquareError('invalid_args', 'Cannot read backend configuration; check SQUARE_BACKENDS_CONFIG');
66
+ }
67
+ try {
68
+ const parsed = JSON.parse(text);
69
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object' || Object.keys(parsed).some(key => key !== 'backends') || parsed.backends === undefined)
70
+ throw new Error();
71
+ return validateBackends(parsed.backends);
72
+ }
73
+ catch {
74
+ throw new SquareError('invalid_args', 'Invalid backend configuration; expected {backends:{name:{command,args?,cwd?,env?}}}');
75
+ }
76
+ }
@@ -0,0 +1,26 @@
1
+ import { type Body } from './model.js';
2
+ /** Offline conversion input only; never a runtime ownership or evidence reader. */
3
+ export interface LegacyOwner {
4
+ readonly participant: string;
5
+ readonly body: Body;
6
+ }
7
+ export interface LegacyHostData {
8
+ readonly owners: readonly LegacyOwner[];
9
+ readonly evidence: readonly LegacyDeliveryEvidence[];
10
+ }
11
+ export interface LegacyDeliveryEvidence {
12
+ readonly v: 1;
13
+ readonly location: string;
14
+ readonly participant: string;
15
+ readonly session: string;
16
+ readonly activity: string;
17
+ readonly kind: 'wake' | 'presentation' | 'lifecycle';
18
+ readonly outcome: string;
19
+ readonly at?: number;
20
+ readonly expiresAt?: number;
21
+ readonly attemptN?: number;
22
+ readonly claimToken?: string;
23
+ readonly [key: string]: unknown;
24
+ }
25
+ /** One frozen snapshot, latest owners only; no age, expiry, or ownership history inference. */
26
+ export declare function readLegacyHostData(location: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<LegacyHostData>;