@astrosheep/square 0.3.92 → 0.3.94

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 (119) hide show
  1. package/dist/activity-feed.d.ts +3 -5
  2. package/dist/activity-feed.js +16 -14
  3. package/dist/artifact.js +2 -2
  4. package/dist/boundary-presentation.d.ts +2 -2
  5. package/dist/catch-decisions.d.ts +4 -6
  6. package/dist/catch-decisions.js +4 -4
  7. package/dist/claude-delivery-status.js +10 -4
  8. package/dist/claude-delivery.js +19 -11
  9. package/dist/claude-mod.js +24 -53
  10. package/dist/cli/context.d.ts +9 -4
  11. package/dist/cli/context.js +30 -25
  12. package/dist/cli/express-presentation.d.ts +3 -1
  13. package/dist/cli/express-presentation.js +26 -14
  14. package/dist/cli/json.d.ts +3 -0
  15. package/dist/cli/json.js +14 -0
  16. package/dist/cli/mcp-server-command.js +2 -2
  17. package/dist/cli/observation-commands.d.ts +1 -3
  18. package/dist/cli/observation-commands.js +43 -56
  19. package/dist/cli/program.js +2 -2
  20. package/dist/cli/registry.js +6 -3
  21. package/dist/cli/square-commands.d.ts +1 -1
  22. package/dist/cli/square-commands.js +46 -54
  23. package/dist/codex-hook.d.ts +3 -2
  24. package/dist/codex-hook.js +13 -4
  25. package/dist/codex-queue.js +4 -4
  26. package/dist/decisions.d.ts +5 -11
  27. package/dist/decisions.js +9 -20
  28. package/dist/delivery-deferral.js +2 -2
  29. package/dist/delivery-health.d.ts +0 -1
  30. package/dist/delivery-health.js +1 -3
  31. package/dist/delivery-operations.d.ts +2 -2
  32. package/dist/delivery-operations.js +38 -38
  33. package/dist/delivery.d.ts +1 -1
  34. package/dist/delivery.js +1 -1
  35. package/dist/harness-links.d.ts +1 -10
  36. package/dist/harness-links.js +144 -109
  37. package/dist/harness.js +4 -13
  38. package/dist/help.js +12 -10
  39. package/dist/inbox.d.ts +9 -22
  40. package/dist/inbox.js +23 -27
  41. package/dist/index.d.ts +1 -3
  42. package/dist/index.js +1 -1
  43. package/dist/mcp.d.ts +3 -0
  44. package/dist/mcp.js +23 -24
  45. package/dist/model.d.ts +8 -15
  46. package/dist/model.js +4 -7
  47. package/dist/notifications.d.ts +1 -1
  48. package/dist/notifications.js +6 -5
  49. package/dist/operation-presentation.d.ts +5 -4
  50. package/dist/operation-presentation.js +50 -45
  51. package/dist/owner.d.ts +11 -0
  52. package/dist/owner.js +32 -0
  53. package/dist/paseo-delivery.d.ts +4 -1
  54. package/dist/paseo-delivery.js +7 -12
  55. package/dist/paseo-state.d.ts +2 -2
  56. package/dist/paseo-state.js +6 -4
  57. package/dist/plugins/opencode/delivery.js +27 -15
  58. package/dist/ports.d.ts +13 -33
  59. package/dist/presentation-operations.js +4 -4
  60. package/dist/presentation.d.ts +5 -8
  61. package/dist/presentation.js +33 -43
  62. package/dist/runtime.d.ts +1 -7
  63. package/dist/runtime.js +0 -14
  64. package/dist/seats.d.ts +2 -3
  65. package/dist/seats.js +7 -15
  66. package/dist/session-lifecycle.d.ts +6 -2
  67. package/dist/session-lifecycle.js +39 -11
  68. package/dist/sqlite-square.d.ts +36 -22
  69. package/dist/sqlite-square.js +138 -161
  70. package/dist/square-api.d.ts +15 -10
  71. package/dist/square-api.js +26 -22
  72. package/dist/square-core.d.ts +0 -5
  73. package/dist/square-core.js +0 -9
  74. package/dist/square-domain.d.ts +4 -2
  75. package/dist/square-domain.js +56 -21
  76. package/dist/square-facade.d.ts +45 -87
  77. package/dist/square-index.d.ts +5 -13
  78. package/dist/square-index.js +13 -38
  79. package/dist/square-location.d.ts +1 -14
  80. package/dist/square-location.js +4 -69
  81. package/dist/square-migration-host-ledger.d.ts +2 -2
  82. package/dist/square-migration-host-ledger.js +6 -5
  83. package/dist/square-migration.d.ts +2 -1
  84. package/dist/square-migration.js +52 -30
  85. package/dist/square-sqlite.d.ts +6 -2
  86. package/dist/square-sqlite.js +54 -28
  87. package/dist/square-tools.d.ts +7 -10
  88. package/dist/square-tools.js +22 -54
  89. package/dist/stream.js +4 -5
  90. package/dist/views.d.ts +21 -10
  91. package/dist/views.js +38 -38
  92. package/dist/wakes.d.ts +1 -2
  93. package/dist/wakes.js +1 -1
  94. package/guides/application-facade-delivery-evidence.md +1 -1
  95. package/guides/claude-native.md +18 -9
  96. package/guides/codex-delivery.md +17 -6
  97. package/guides/mcp-connection-lifetime.md +1 -1
  98. package/guides/mcp.md +3 -3
  99. package/guides/migration.md +142 -151
  100. package/guides/operation-cancellation.md +1 -1
  101. package/guides/participant-ownership.md +78 -47
  102. package/guides/pi-tools.md +33 -4
  103. package/guides/primitive-history-consolidation.md +1 -1
  104. package/guides/row-storage-design.md +8 -8
  105. package/guides/square-api.md +244 -88
  106. package/package.json +2 -1
  107. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  108. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  109. package/plugins/opencode/delivery.ts +27 -17
  110. package/plugins/paseo/client/palette.ts +18 -18
  111. package/plugins/paseo/client/square-message.tsx +17 -23
  112. package/plugins/pi/square-pi.js +17 -17
  113. package/dist/square-backend.d.ts +0 -13
  114. package/dist/square-backend.js +0 -1
  115. package/dist/square-stdio.d.ts +0 -22
  116. package/dist/square-stdio.js +0 -235
  117. package/dist/stdio-server.d.ts +0 -3
  118. package/dist/stdio-server.js +0 -212
  119. package/guides/backends.md +0 -193
@@ -5,8 +5,7 @@ import { randomUUID } from 'node:crypto';
5
5
  import { withFileLock } from './file-lock.js';
6
6
  import { canonicalPath } from './canonical-path.js';
7
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. */
8
+ import { validateOwner } from './model.js';
10
9
  export function squareIndexPath(env = process.env) {
11
10
  const configured = env.SQUARE_INDEX?.trim();
12
11
  return path.resolve(configured ?? path.join(os.homedir(), '.square', 'index.ndjsonl'));
@@ -18,24 +17,17 @@ function validEntry(value) {
18
17
  if (value === null || typeof value !== 'object' || Array.isArray(value))
19
18
  return undefined;
20
19
  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))
20
+ if (typeof row.location !== 'string' || !path.isAbsolute(row.location) || typeof row.owner !== 'string')
23
21
  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
22
  try {
31
- validateBody(body);
23
+ validateOwner(row.owner);
32
24
  }
33
25
  catch {
34
26
  return undefined;
35
27
  }
36
- if (Object.keys(raw).some((key) => !['channel', 'session', 'agent'].includes(key)))
28
+ if (Object.keys(row).some((key) => !['location', 'owner'].includes(key)))
37
29
  return undefined;
38
- return { location: row.location, body };
30
+ return { location: row.location, owner: row.owner };
39
31
  }
40
32
  async function readEntries(index) {
41
33
  let content;
@@ -56,25 +48,13 @@ async function readEntries(index) {
56
48
  if (entry !== undefined)
57
49
  entries.push(entry);
58
50
  }
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
- }
51
+ catch { /* Disposable discovery input. */ }
63
52
  }
64
53
  return entries;
65
54
  }
66
55
  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;
56
+ return query.owner === undefined || entry.owner === query.owner;
76
57
  }
77
- /** Read and verify the disposable index. Paths are canonicalized on access and duplicates collapse. */
78
58
  export async function indexedSquareEntries(query = {}, env = process.env) {
79
59
  const entries = await readEntries(squareIndexPath(env));
80
60
  const result = [];
@@ -89,11 +69,11 @@ export async function indexedSquareEntries(query = {}, env = process.env) {
89
69
  catch {
90
70
  continue;
91
71
  }
92
- const key = JSON.stringify([location, entry.body.channel, entry.body.session, entry.body.agent ?? null]);
72
+ const key = JSON.stringify([location, entry.owner]);
93
73
  if (seen.has(key))
94
74
  continue;
95
75
  seen.add(key);
96
- result.push({ location, body: entry.body });
76
+ result.push({ location, owner: entry.owner });
97
77
  }
98
78
  return result;
99
79
  }
@@ -101,15 +81,15 @@ export async function indexedSquarePaths(query = {}, env = process.env) {
101
81
  return [...new Set((await indexedSquareEntries(query, env)).map((entry) => entry.location))];
102
82
  }
103
83
  /** 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);
84
+ export async function rememberSquarePath(location, owner, index) {
85
+ validateOwner(owner);
106
86
  const canonical = await canonicalPath(location);
107
87
  await fs.mkdir(path.dirname(index), { recursive: true });
108
88
  return await withFileLock(lockPath(index), { retryMs: 10, skipIfBusy: true }, async () => {
109
89
  const entries = await readEntries(index);
110
- if (entries.some((entry) => entry.location === canonical && sameBody(entry.body, body)))
90
+ if (entries.some((entry) => entry.location === canonical && entry.owner === owner))
111
91
  return false;
112
- const next = [...entries, { location: canonical, body }];
92
+ const next = [...entries, { location: canonical, owner }];
113
93
  const temporary = `${index}.${process.pid}.${randomUUID()}.tmp`;
114
94
  try {
115
95
  await fs.writeFile(temporary, next.map((entry) => JSON.stringify(entry)).join('\n') + '\n', { flag: 'wx', mode: 0o600 });
@@ -121,11 +101,6 @@ export async function rememberSquarePath(location, body, index) {
121
101
  return true;
122
102
  }) ?? false;
123
103
  }
124
- /** Index loss is tolerated by every caller; this observer is only a wake hint for rediscovery. */
125
104
  export function observeSquareIndex(env = process.env) {
126
105
  return observeFileMetadata(squareIndexPath(env));
127
106
  }
128
- export function bodyKey(body) {
129
- validateBody(body);
130
- return JSON.stringify([body.channel, body.session, body.agent ?? null]);
131
- }
@@ -1,15 +1,2 @@
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;
1
+ import type { SquareLocation } from './square-facade.js';
13
2
  export declare function squareLocation(input: SquareLocation): string;
14
- export declare function localSquareLocation(input: SquareLocation): string;
15
- export declare function backendConfiguration(input: SquareAtInput): Promise<SquareBackends>;
@@ -1,76 +1,11 @@
1
1
  import path from 'node:path';
2
- import os from 'node:os';
3
- import { readFile } from 'node:fs/promises';
4
2
  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
3
  export function squareLocation(input) {
24
4
  const selected = input.path ?? (input.env ?? process.env).SQUARE_LOCATION;
25
5
  if (selected === undefined || selected.trim() === '')
26
6
  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
- }
7
+ // Windows roots include drive-relative prefixes such as D:.
8
+ if (path.win32.parse(selected).root === '' && /^[A-Za-z][A-Za-z0-9+.-]*:/.test(selected))
9
+ throw new SquareError('invalid_args', 'Expected a local square artifact path');
10
+ return path.resolve(input.cwd ?? process.cwd(), selected);
76
11
  }
@@ -1,8 +1,8 @@
1
- import { type Body } from './model.js';
1
+ import { type HarnessCoordinate } from './owner.js';
2
2
  /** Offline conversion input only; never a runtime ownership or evidence reader. */
3
3
  export interface LegacyOwner {
4
4
  readonly participant: string;
5
- readonly body: Body;
5
+ readonly coordinate: HarnessCoordinate;
6
6
  }
7
7
  export interface LegacyHostData {
8
8
  readonly owners: readonly LegacyOwner[];
@@ -5,7 +5,8 @@ import path from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import { canonicalPath } from './canonical-path.js';
7
7
  import { inSqliteTransaction } from './sqlite-transaction.js';
8
- import { nameKey, validateBody } from './model.js';
8
+ import { harnessOwner } from './owner.js';
9
+ import { nameKey } from './model.js';
9
10
  const LEDGER_FILENAME = 'host-ledger.sqlite';
10
11
  const LEDGER_APPLICATION_ID = 0x5351484c;
11
12
  const LEDGER_VERSION = 2;
@@ -144,11 +145,11 @@ export async function readLegacyHostData(location, env = process.env, signal) {
144
145
  if (value.v !== 1 || value.location !== canonical || typeof value.participant !== 'string'
145
146
  || !finite(at))
146
147
  throw invalid('owner fields are malformed.');
147
- const body = { channel: value.channel, session: value.session, ...(value.agent === undefined ? {} : { agent: value.agent }) };
148
- validateBody(body);
148
+ const coordinate = { channel: value.channel, session: value.session, ...(value.agent === undefined ? {} : { agent: value.agent }) };
149
+ harnessOwner(coordinate);
149
150
  projections(row, { location_key: recordKey(canonical), participant_key: recordKey(nameKey(value.participant)),
150
- channel_key: recordKey(body.channel), session_key: recordKey(body.session), effective_at: at });
151
- return { participant: value.participant, body };
151
+ channel_key: recordKey(coordinate.channel), session_key: recordKey(coordinate.session), effective_at: at });
152
+ return { participant: value.participant, coordinate };
152
153
  });
153
154
  const evidence = database.prepare("SELECT * FROM host_evidence WHERE location_key=? AND kind IN ('wake','presentation') ORDER BY seq").all(recordKey(canonical)).map(row => {
154
155
  const value = decodeEvidence(record(row));
@@ -20,7 +20,8 @@ export interface MigrationOutcome {
20
20
  /**
21
21
  * Explicit conversion of the supported legacy formats. Row-backed versions 2 and 3 are
22
22
  * preflighted read-only, then receive ownership/evidence and deferral storage in one
23
- * format-5 transaction. Format 4 adds only the empty deferral table and its index.
23
+ * format-7 transaction. Formats 4/5 gain attachment; format 4 also gains empty deferrals.
24
+ * All legacy seats are encoded as opaque owners, preserving format-6 attachment.
24
25
  * Legacy delivery evidence is copied from HostLedger only when its owner/session mapping is
25
26
  * unique; no source record is retired. Version 1 is not a supported source for this format transition.
26
27
  */
@@ -2,11 +2,12 @@ import { createHash } from 'node:crypto';
2
2
  import { openExistingSquareDatabase } from './artifact.js';
3
3
  import { inSqliteTransaction } from './sqlite-transaction.js';
4
4
  import { canonicalPath, canonicalPathSync } from './canonical-path.js';
5
- import { formatActivityId, isWakeRouteKind, nameKey, sameBody, validateBody } from './model.js';
5
+ import { harnessOwner, decodeHarnessOwner } from './owner.js';
6
+ import { formatActivityId, isWakeRouteKind, nameKey } from './model.js';
6
7
  import { parseActivityId } from './square-core.js';
7
8
  import { readLegacyHostData } from './square-migration-host-ledger.js';
8
9
  import { legacyRowFramingIssue, FORMAT4_ADDED_OBJECTS } from './square-legacy-framing.js';
9
- import { APPLICATION_ID, ACTIVITIES_TABLE_SQL, ACTIVITY_INDEXES_SQL, CURRENT_USER_VERSION, V2_USER_VERSION, V3_USER_VERSION, V4_USER_VERSION, SCHEMA_SQL, FORMAT4_SCHEMA_SQL, DELIVERY_DEFERRALS_SQL, validateRowFraming, inspectSqlite, createSqliteDriver, invalidArtifact, validateStoredActivity } from './square-sqlite.js';
10
+ import { APPLICATION_ID, ACTIVITIES_TABLE_SQL, ACTIVITY_INDEXES_SQL, CURRENT_USER_VERSION, V2_USER_VERSION, V3_USER_VERSION, V4_USER_VERSION, V5_USER_VERSION, V6_USER_VERSION, SCHEMA_SQL, FORMAT4_SCHEMA_SQL, DELIVERY_DEFERRALS_SQL, validateRowFraming, inspectSqlite, createSqliteDriver, invalidArtifact, validateStoredActivity } from './square-sqlite.js';
10
11
  function isObject(value) {
11
12
  return typeof value === 'object' && value !== null && !Array.isArray(value);
12
13
  }
@@ -23,6 +24,9 @@ function isNonNegativeInteger(value) {
23
24
  function isStringArray(value) {
24
25
  return Array.isArray(value) && value.every((item) => typeof item === 'string');
25
26
  }
27
+ function sameCoordinate(left, right) {
28
+ return left.channel === right.channel && left.session === right.session && left.agent === right.agent;
29
+ }
26
30
  function validateRecord(value, item) {
27
31
  return isObject(value) && Object.entries(value).every(([key, candidate]) => key.length > 0 && item(candidate));
28
32
  }
@@ -142,7 +146,7 @@ function rebuildV2Activities(database) {
142
146
  }
143
147
  function validateCurrentFraming(database, location) {
144
148
  validateRowFraming(database, CURRENT_USER_VERSION, location);
145
- createSqliteDriver(database).header();
149
+ inspectSqlite(database);
146
150
  }
147
151
  function assertSource(condition, detail = 'row is malformed.') {
148
152
  if (!condition)
@@ -190,12 +194,12 @@ function decodeLegacyRoute(row) {
190
194
  assertSource(row.participant_key_json === JSON.stringify(nameKey(participant)), 'route participant key is malformed.');
191
195
  const sessionId = decodeJsonString(row.session_json);
192
196
  sourceFinite(row.updated_at);
193
- const body = { channel: parsed.channel, session: sessionId, ...(parsed.extra.agent === undefined ? {} : { agent: parsed.extra.agent }) };
194
- validateBody(body);
197
+ const coordinate = { channel: parsed.channel, session: sessionId, ...(parsed.extra.agent === undefined ? {} : { agent: parsed.extra.agent }) };
198
+ harnessOwner(coordinate);
195
199
  const address = parsed.address;
196
200
  if (row.kind === 'claude-native')
197
201
  assertSource(address.sessionId === undefined || address.sessionId === sessionId, 'native route session conflicts with its body.');
198
- return { location: parsed.location, participant, body,
202
+ return { location: parsed.location, participant, coordinate,
199
203
  wakeAddress: { kind: row.kind, address: { ...address, ...(row.kind === 'claude-native' ? { sessionId } : {}) } } };
200
204
  }
201
205
  function validateLegacyLease(value) {
@@ -235,14 +239,15 @@ function planSeats(location, roster, routes, owners, driver) {
235
239
  const joined = establishingJoin(driver, participant);
236
240
  const candidates = localRoutes.filter((route) => nameKey(route.participant) === nameKey(participant.name));
237
241
  const owner = owners.find(row => nameKey(row.participant) === nameKey(participant.name));
238
- const body = owner?.body ?? candidates[0]?.body;
239
- if (body === undefined || !candidates.every(route => sameBody(route.body, body)))
242
+ const coordinates = [...candidates.map(route => route.coordinate), ...(owner === undefined ? [] : [owner.coordinate])];
243
+ if (coordinates.length === 0 || !coordinates.every(coordinate => sameCoordinate(coordinate, coordinates[0])))
240
244
  continue;
241
245
  const address = uniqueSeatAddress(candidates);
242
246
  seats.push({
243
247
  participant: participant.name,
244
- body,
248
+ owner: harnessOwner(coordinates[0]),
245
249
  generation: formatActivityId(joined),
250
+ attachment: 'attached',
246
251
  ...(address === undefined ? {} : { wakeAddress: address }),
247
252
  });
248
253
  }
@@ -321,7 +326,7 @@ function inspectLegacyRowArtifact(database, location, host) {
321
326
  routes: routes.length,
322
327
  recoveredSeats: seats.length,
323
328
  unownedStanding: standingCount - seats.length,
324
- sharedBodySeats: seats.filter(seat => seats.some(other => other !== seat && sameBody(other.body, seat.body))).length,
329
+ sharedBodySeats: seats.filter(seat => seats.some(other => other !== seat && other.owner === seat.owner)).length,
325
330
  skippedEvidence: host.evidence.length - deliveries.length,
326
331
  };
327
332
  const sourceToken = legacySourceToken(database);
@@ -373,12 +378,14 @@ function planLegacyDeliveries(records, seats, driver) {
373
378
  const native = source.kind === 'wake' && source.nativeDelivery !== undefined;
374
379
  if (!participant?.joined && !(participant?.done && native))
375
380
  continue;
376
- if (participant.joined && (seat === undefined || seat.body.session !== source.session
377
- || source.agent !== seat.body.agent || source.channel !== undefined && source.channel !== seat.body.channel))
381
+ const seatCoordinate = seat === undefined ? undefined : decodeHarnessOwner(seat.owner);
382
+ if (participant.joined && (seatCoordinate === undefined || seatCoordinate.session !== source.session
383
+ || seatCoordinate.agent !== source.agent || source.channel !== undefined && seatCoordinate.channel !== source.channel))
378
384
  continue;
379
385
  const harness = source.nativeDelivery?.harness;
380
- if (native && seat !== undefined && harness !==
381
- (seat.body.channel === 'claude-code' ? 'claude' : seat.body.channel === 'opencode' ? 'opencode' : undefined))
386
+ if (native && source.channel !== undefined && source.channel !== (harness === 'claude' ? 'claude-code' : 'opencode'))
387
+ continue;
388
+ if (native && seat !== undefined && harness !== undefined && seat.owner !== harnessOwner({ channel: harness === 'claude' ? 'claude-code' : 'opencode', session: source.session, ...(source.agent === undefined ? {} : { agent: source.agent }) }))
382
389
  continue;
383
390
  const joined = establishingJoin(driver, participant);
384
391
  const generation = formatActivityId(joined);
@@ -388,11 +395,9 @@ function planLegacyDeliveries(records, seats, driver) {
388
395
  const done = driver.activityHeads({ actors: [participant.name], kinds: ['done'] }, { edge: 'newest', limit: 1 }).items[0];
389
396
  if (done === undefined || index >= done.index)
390
397
  continue;
391
- const body = { channel: harness === 'claude' ? 'claude-code' : 'opencode', session: source.session,
392
- ...(source.agent === undefined ? {} : { agent: source.agent }) };
393
- if (source.channel !== undefined && source.channel !== body.channel)
394
- continue;
395
- const coordinate = JSON.stringify([body.channel, body.session, body.agent]);
398
+ const owner = harnessOwner({ channel: harness === 'claude' ? 'claude-code' : 'opencode', session: source.session,
399
+ ...(source.agent === undefined ? {} : { agent: source.agent }) });
400
+ const coordinate = JSON.stringify([owner]);
396
401
  const key = nameKey(participant.name), previous = completedBodies.get(key);
397
402
  completedBodies.set(key, previous === undefined || previous === coordinate ? coordinate : null);
398
403
  }
@@ -429,7 +434,6 @@ function planLegacyDeliveries(records, seats, driver) {
429
434
  }
430
435
  return deliveries.filter(row => row.claimToken === undefined || tokens.get(row.claimToken) === 1);
431
436
  }
432
- const V4_REBUILT_TABLES = ['square_meta', 'catch_leases'];
433
437
  function tableDefinition(name) {
434
438
  const definition = SCHEMA_SQL.split(';').map((statement) => statement.trim()).find((statement) => statement.startsWith(`CREATE TABLE ${name} (`));
435
439
  if (definition === undefined)
@@ -439,16 +443,32 @@ function tableDefinition(name) {
439
443
  function currentAdditionStatements() {
440
444
  return SCHEMA_SQL.split(';').map((statement) => statement.trim()).filter((statement) => {
441
445
  const match = /^(?:CREATE (?:UNIQUE )?TABLE|CREATE (?:UNIQUE )?INDEX)\s+([a-z_]+)/i.exec(statement);
442
- return match !== null && FORMAT4_ADDED_OBJECTS.has(match[1]);
446
+ return match !== null && (FORMAT4_ADDED_OBJECTS.has(match[1]) || match[1] === 'seat_owner');
443
447
  }).map((statement) => `${statement};`);
444
448
  }
449
+ function migrateSeatOwnership(database, source) {
450
+ const rows = database.prepare('SELECT * FROM seats ORDER BY participant_key_json').all();
451
+ database.exec('ALTER TABLE seats RENAME TO seats_legacy;');
452
+ database.exec(tableDefinition('seats'));
453
+ database.exec('CREATE INDEX seat_owner ON seats(owner_json);');
454
+ const insert = database.prepare('INSERT INTO seats(participant_key_json,participant_json,owner_json,generation_json,wake_address_json,attachment) VALUES(?,?,?,?,?,?)');
455
+ for (const row of rows) {
456
+ const channel = decodeJsonString(row.channel_json, true), session = decodeJsonString(row.session_json, true);
457
+ const agent = row.agent_json === null ? undefined : decodeJsonString(row.agent_json, true);
458
+ const owner = harnessOwner({ channel, session, ...(agent === undefined ? {} : { agent }) });
459
+ const attachment = source === 6 ? row.attachment : 'attached';
460
+ assertSource(attachment === 'attached' || attachment === 'detached', 'seat attachment is malformed.');
461
+ insert.run(row.participant_key_json, row.participant_json, JSON.stringify(owner), row.generation_json, row.wake_address_json, attachment);
462
+ }
463
+ database.exec('DROP TABLE seats_legacy;');
464
+ }
445
465
  function importOwnershipAndEvidence(database, plan) {
446
466
  for (const generation of plan.leaseGenerations) {
447
467
  database.prepare('UPDATE catch_leases SET generation_json=? WHERE owner_json=?').run(JSON.stringify(generation.generation), generation.ownerJson);
448
468
  }
449
- const insertSeat = database.prepare('INSERT INTO seats(participant_key_json,participant_json,channel_json,session_json,agent_json,generation_json,wake_address_json) VALUES(?,?,?,?,?,?,?)');
469
+ const insertSeat = database.prepare('INSERT INTO seats(participant_key_json,participant_json,owner_json,generation_json,wake_address_json,attachment) VALUES(?,?,?,?,?,?)');
450
470
  for (const seat of plan.seats) {
451
- insertSeat.run(JSON.stringify(nameKey(seat.participant)), JSON.stringify(seat.participant), JSON.stringify(seat.body.channel), JSON.stringify(seat.body.session), seat.body.agent === undefined ? null : JSON.stringify(seat.body.agent), JSON.stringify(seat.generation), seat.wakeAddress === undefined ? null : JSON.stringify(seat.wakeAddress));
471
+ insertSeat.run(JSON.stringify(nameKey(seat.participant)), JSON.stringify(seat.participant), JSON.stringify(seat.owner), JSON.stringify(seat.generation), seat.wakeAddress === undefined ? null : JSON.stringify(seat.wakeAddress), seat.attachment);
452
472
  }
453
473
  const insertDelivery = database.prepare('INSERT INTO delivery_evidence(participant_key_json,participant_json,generation_json,act_index,kind,attempt_n,outcome,at,expires_at,claim_token_json,payload_json) VALUES(?,?,?,?,?,?,?,?,?,?,?)');
454
474
  for (const evidence of plan.deliveries) {
@@ -481,10 +501,9 @@ function validateMigrationSource(database, location) {
481
501
  throw invalidArtifact(issue);
482
502
  return before.userVersion;
483
503
  }
484
- if (before.userVersion === V4_USER_VERSION) {
485
- validateRowFraming(database, V4_USER_VERSION, location);
486
- inspectSqlite(database);
487
- return V4_USER_VERSION;
504
+ if (before.userVersion === V4_USER_VERSION || before.userVersion === V5_USER_VERSION || before.userVersion === V6_USER_VERSION) {
505
+ validateRowFraming(database, before.userVersion, location);
506
+ return before.userVersion;
488
507
  }
489
508
  if (before.userVersion === CURRENT_USER_VERSION) {
490
509
  validateCurrentFraming(database, location);
@@ -495,7 +514,8 @@ function validateMigrationSource(database, location) {
495
514
  /**
496
515
  * Explicit conversion of the supported legacy formats. Row-backed versions 2 and 3 are
497
516
  * preflighted read-only, then receive ownership/evidence and deferral storage in one
498
- * format-5 transaction. Format 4 adds only the empty deferral table and its index.
517
+ * format-7 transaction. Formats 4/5 gain attachment; format 4 also gains empty deferrals.
518
+ * All legacy seats are encoded as opaque owners, preserving format-6 attachment.
499
519
  * Legacy delivery evidence is copied from HostLedger only when its owner/session mapping is
500
520
  * unique; no source record is retired. Version 1 is not a supported source for this format transition.
501
521
  */
@@ -506,11 +526,13 @@ export async function migrateSquareFile(location, signal, env = process.env) {
506
526
  const source = await inSqliteTransaction(database, false, () => validateMigrationSource(database, squarePath), value => value, signal);
507
527
  if (source === undefined)
508
528
  return { status: 'already-current', standing: standing(database) };
509
- if (source === V4_USER_VERSION) {
529
+ if (source === V4_USER_VERSION || source === V5_USER_VERSION || source === V6_USER_VERSION) {
510
530
  return await inSqliteTransaction(database, true, () => {
511
531
  assertSource(validateMigrationSource(database, squarePath) === source, 'source format changed during migration preflight.');
512
532
  }, () => {
513
- database.exec(DELIVERY_DEFERRALS_SQL);
533
+ if (source === V4_USER_VERSION)
534
+ database.exec(DELIVERY_DEFERRALS_SQL);
535
+ migrateSeatOwnership(database, source);
514
536
  database.exec(`PRAGMA user_version = ${CURRENT_USER_VERSION};`);
515
537
  validateCurrentFraming(database, squarePath);
516
538
  return { status: 'migrated', standing: standing(database) };
@@ -21,7 +21,9 @@ export declare const APPLICATION_ID = 1397838405;
21
21
  export declare const V2_USER_VERSION = 2;
22
22
  export declare const V3_USER_VERSION = 3;
23
23
  export declare const V4_USER_VERSION = 4;
24
- export declare const CURRENT_USER_VERSION = 5;
24
+ export declare const V5_USER_VERSION = 5;
25
+ export declare const V6_USER_VERSION = 6;
26
+ export declare const CURRENT_USER_VERSION = 7;
25
27
  export declare const V1_SNAPSHOT_TABLE = "square_snapshot";
26
28
  export interface LegacyFraming {
27
29
  readonly applicationId: number;
@@ -39,11 +41,13 @@ export declare const ACTIVITIES_TABLE_SQL = "CREATE TABLE activities (\n act_in
39
41
  export declare const ACTIVITY_INDEXES_SQL: readonly ["CREATE INDEX activity_kind_index ON activities(kind, act_index)", "CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index)", "CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say'", "CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say'", "CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell'"];
40
42
  export declare const DELIVERY_DEFERRALS_SQL = "CREATE TABLE delivery_deferrals (\n participant_key_json TEXT COLLATE BINARY NOT NULL,\n participant_json TEXT NOT NULL,\n generation_json TEXT NOT NULL,\n deferral_id_json TEXT NOT NULL,\n match_json TEXT NOT NULL CHECK (json_valid(match_json)),\n expires_at REAL NOT NULL,\n PRIMARY KEY (participant_key_json,generation_json,deferral_id_json)\n) WITHOUT ROWID;\nCREATE INDEX delivery_deferral_expiry ON delivery_deferrals(participant_key_json,generation_json,expires_at);";
41
43
  export declare const SCHEMA_SQL: string;
44
+ export declare const FORMAT6_SCHEMA_SQL: string;
45
+ export declare const FORMAT5_SCHEMA_SQL: string;
42
46
  export declare const FORMAT4_SCHEMA_SQL: string;
43
47
  export declare function invalidArtifact(detail: string): SquareError;
44
48
  export declare function validateStoredActivity(value: unknown): StoredAct;
45
49
  export declare function validateWatchLease(value: unknown): WatchLease;
46
- export declare function validateRowFraming(db: DatabaseSync, version: 2 | 3 | 4 | 5, location?: string): void;
50
+ export declare function validateRowFraming(db: DatabaseSync, version: 2 | 3 | 4 | 5 | 6 | 7, location?: string): void;
47
51
  export declare function validateSqliteHeader(db: DatabaseSync, location?: string): SquareHeader;
48
52
  export interface SqliteDriver extends SquareRead, LandingWriter {
49
53
  readonly changed: boolean;