@astrosheep/square 0.3.90 → 0.3.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/maintenance-commands.js +5 -2
- package/dist/help.js +2 -2
- package/dist/square-migration.d.ts +4 -0
- package/dist/square-migration.js +62 -51
- package/guides/migration.md +53 -27
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
|
@@ -10,10 +10,13 @@ export const migrateCommand = {
|
|
|
10
10
|
async execute(_intent, context) {
|
|
11
11
|
const squarePath = requireSquarePath(context);
|
|
12
12
|
const outcome = await migrateSquare(squareInput(context));
|
|
13
|
-
const body = outcome.status === 'migrated'
|
|
13
|
+
const body = outcome.status === 'migrated'
|
|
14
14
|
? [
|
|
15
15
|
'✓ migrated to current row storage',
|
|
16
|
-
|
|
16
|
+
...(outcome.counts === undefined ? [] : [
|
|
17
|
+
` · ${outcome.counts.activities} activities · ${outcome.counts.receipts} receipts · ${outcome.counts.leases} leases · ${outcome.counts.routes} routes`,
|
|
18
|
+
` · ${outcome.counts.recoveredSeats} recovered seats · ${outcome.counts.unownedStanding} unowned standing · ${outcome.counts.sharedBodySeats} shared-body seats · ${outcome.counts.skippedEvidence} skipped evidence`,
|
|
19
|
+
]),
|
|
17
20
|
].join('\n')
|
|
18
21
|
: '· already current format — nothing changed';
|
|
19
22
|
return { output: withPathOutput(squarePath, body, { participantCount: outcome.standing }) };
|
package/dist/help.js
CHANGED
|
@@ -92,8 +92,8 @@ const COMMANDS = [
|
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
names: ['migrate'], usage: 'migrate', usesSquare: true, group: 'maintenance',
|
|
95
|
-
summary: 'Convert
|
|
96
|
-
details: ['For the selected .square artifact only. Stop sessions using this artifact before conversion. Back up the stopped artifact, migrate it, then run square --location <path> doctor before starting updated clients.', '
|
|
95
|
+
summary: 'Convert a supported older square to the current format in place.',
|
|
96
|
+
details: ['For the selected .square artifact only. Stop sessions using this artifact before conversion. Back up the stopped artifact, migrate it, then run square --location <path> doctor before starting updated clients.', 'Versions 2, 3 and 4 are accepted; version 1, unrelated or unsupported artifacts are refused without a write. Names whose own owner and route evidence agrees may share a body; names with missing or conflicting ownership stay standing without write or delivery authority. Migration reports recovered seats, unowned standing names, shared-body seats and skipped evidence. There is no --check, --backup or --force.'],
|
|
97
97
|
},
|
|
98
98
|
];
|
|
99
99
|
function definitionFor(command) {
|
|
@@ -6,6 +6,10 @@ export interface MigrationCounts {
|
|
|
6
6
|
readonly receipts: number;
|
|
7
7
|
readonly leases: number;
|
|
8
8
|
readonly routes: number;
|
|
9
|
+
readonly recoveredSeats: number;
|
|
10
|
+
readonly unownedStanding: number;
|
|
11
|
+
readonly sharedBodySeats: number;
|
|
12
|
+
readonly skippedEvidence: number;
|
|
9
13
|
}
|
|
10
14
|
export interface MigrationOutcome {
|
|
11
15
|
readonly status: 'migrated' | 'already-current';
|
package/dist/square-migration.js
CHANGED
|
@@ -229,25 +229,24 @@ function establishingJoin(driver, participant) {
|
|
|
229
229
|
return join.index;
|
|
230
230
|
}
|
|
231
231
|
function planSeats(location, roster, routes, owners, driver) {
|
|
232
|
-
const
|
|
233
|
-
|
|
232
|
+
const seats = [];
|
|
233
|
+
const localRoutes = routes.filter(route => canonicalPathSync(route.location) === location);
|
|
234
|
+
for (const participant of roster.filter((participant) => participant.joined)) {
|
|
234
235
|
const joined = establishingJoin(driver, participant);
|
|
235
|
-
const candidates =
|
|
236
|
+
const candidates = localRoutes.filter((route) => nameKey(route.participant) === nameKey(participant.name));
|
|
236
237
|
const owner = owners.find(row => nameKey(row.participant) === nameKey(participant.name));
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const shared = [...owners, ...routes.filter(route => canonicalPathSync(route.location) === location)]
|
|
241
|
-
.some(row => nameKey(row.participant) !== nameKey(participant.name) && sameBody(row.body, body));
|
|
242
|
-
assertSource(!shared, `standing participant ${participant.name} shares a legacy body with another owner.`);
|
|
238
|
+
const body = owner?.body ?? candidates[0]?.body;
|
|
239
|
+
if (body === undefined || !candidates.every(route => sameBody(route.body, body)))
|
|
240
|
+
continue;
|
|
243
241
|
const address = uniqueSeatAddress(candidates);
|
|
244
|
-
|
|
242
|
+
seats.push({
|
|
245
243
|
participant: participant.name,
|
|
246
244
|
body,
|
|
247
245
|
generation: formatActivityId(joined),
|
|
248
246
|
...(address === undefined ? {} : { wakeAddress: address }),
|
|
249
|
-
};
|
|
250
|
-
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return seats;
|
|
251
250
|
}
|
|
252
251
|
function tableToken(database, table, order) {
|
|
253
252
|
const rows = database.prepare(`SELECT * FROM ${table} ORDER BY ${order}`).all();
|
|
@@ -313,15 +312,20 @@ function inspectLegacyRowArtifact(database, location, host) {
|
|
|
313
312
|
leaseGenerations.push({ ownerJson: row.owner_json, generation: formatActivityId(ownerParticipant.lastJoinIndex) });
|
|
314
313
|
}
|
|
315
314
|
const seats = planSeats(location, roster, routes, host.owners, driver);
|
|
315
|
+
const standingCount = roster.filter(participant => participant.joined).length;
|
|
316
|
+
const deliveries = planLegacyDeliveries(host.evidence, seats, driver);
|
|
316
317
|
const counts = {
|
|
317
318
|
activities: activityCount,
|
|
318
319
|
receipts: Number(database.prepare('SELECT COUNT(*) AS n FROM receipts').get().n),
|
|
319
320
|
leases: leaseOwners.length,
|
|
320
321
|
routes: routes.length,
|
|
322
|
+
recoveredSeats: seats.length,
|
|
323
|
+
unownedStanding: standingCount - seats.length,
|
|
324
|
+
sharedBodySeats: seats.filter(seat => seats.some(other => other !== seat && sameBody(other.body, seat.body))).length,
|
|
325
|
+
skippedEvidence: host.evidence.length - deliveries.length,
|
|
321
326
|
};
|
|
322
327
|
const sourceToken = legacySourceToken(database);
|
|
323
|
-
return { standing:
|
|
324
|
-
deliveries: planLegacyDeliveries(host.evidence, seats, driver) };
|
|
328
|
+
return { standing: standingCount, seats, leaseGenerations, sourceToken, counts, deliveries };
|
|
325
329
|
}
|
|
326
330
|
function legacyPayloadHash(payload) {
|
|
327
331
|
return createHash('sha256').update(payload).digest('hex');
|
|
@@ -348,47 +352,49 @@ function legacyDeliveryPayload(source, current) {
|
|
|
348
352
|
}
|
|
349
353
|
function planLegacyDeliveries(records, seats, driver) {
|
|
350
354
|
const byGroup = new Map();
|
|
351
|
-
const
|
|
355
|
+
const completedBodies = new Map();
|
|
352
356
|
for (const source of records) {
|
|
357
|
+
// Validate structure even when no surviving owner can accept this row.
|
|
358
|
+
assertSource(source.kind === 'wake' || source.kind === 'presentation', 'delivery evidence kind is unsupported.');
|
|
359
|
+
assertSource(['dispatching', 'accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(source.outcome), 'delivery evidence outcome is unsupported.');
|
|
360
|
+
assertSource(source.session.length > 0
|
|
361
|
+
&& (source.channel === undefined || typeof source.channel === 'string' && source.channel.length > 0)
|
|
362
|
+
&& (source.agent === undefined || typeof source.agent === 'string' && source.agent.length > 0), 'delivery evidence body is malformed.');
|
|
363
|
+
const index = parseActivityId(source.activity);
|
|
364
|
+
assertSource(index !== undefined, 'delivery evidence activity id is malformed.');
|
|
365
|
+
const at = sourceFinite(source.at);
|
|
366
|
+
const expiresAt = source.expiresAt === undefined ? undefined : sourceFinite(source.expiresAt);
|
|
367
|
+
const claimToken = source.claimToken;
|
|
368
|
+
assertSource(claimToken === undefined || typeof claimToken === 'string' && claimToken.length > 0, 'delivery claim token is malformed.');
|
|
369
|
+
assertSource(source.attemptN === undefined || Number.isSafeInteger(source.attemptN) && source.attemptN >= 0, 'delivery attempt number is malformed.');
|
|
353
370
|
const participant = driver.participant(source.participant);
|
|
371
|
+
const seat = seats.find(seat => nameKey(seat.participant) === nameKey(source.participant));
|
|
372
|
+
const payload = legacyDeliveryPayload(source, seat?.wakeAddress);
|
|
354
373
|
const native = source.kind === 'wake' && source.nativeDelivery !== undefined;
|
|
355
|
-
if (!participant?.joined && !native)
|
|
356
|
-
continue;
|
|
357
|
-
|
|
374
|
+
if (!participant?.joined && !(participant?.done && native))
|
|
375
|
+
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))
|
|
378
|
+
continue;
|
|
379
|
+
const harness = source.nativeDelivery?.harness;
|
|
380
|
+
if (native && seat !== undefined && harness !==
|
|
381
|
+
(seat.body.channel === 'claude-code' ? 'claude' : seat.body.channel === 'opencode' ? 'opencode' : undefined))
|
|
382
|
+
continue;
|
|
358
383
|
const joined = establishingJoin(driver, participant);
|
|
359
384
|
const generation = formatActivityId(joined);
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
assertSource(seat !== undefined && seat.body.session === source.session
|
|
363
|
-
&& source.agent === seat.body.agent && (source.channel === undefined || source.channel === seat.body.channel), `delivery evidence for standing participant ${source.participant} has no unique owner.`);
|
|
364
|
-
if (native && seat !== undefined)
|
|
365
|
-
assertSource(source.nativeDelivery?.harness ===
|
|
366
|
-
(seat.body.channel === 'claude-code' ? 'claude' : seat.body.channel === 'opencode' ? 'opencode' : undefined), 'native delivery harness conflicts with its owner body.');
|
|
367
|
-
const index = parseActivityId(source.activity);
|
|
368
|
-
assertSource(index !== undefined && index > joined && driver.activity(index)?.kind === 'say'
|
|
369
|
-
&& driver.directedAt(participant.name, index) !== undefined, `delivery evidence for ${source.participant} has no unique activity in its join generation.`);
|
|
370
|
-
const payload = legacyDeliveryPayload(source, seat?.wakeAddress);
|
|
385
|
+
if (index <= joined || driver.activity(index)?.kind !== 'say' || driver.directedAt(participant.name, index) === undefined)
|
|
386
|
+
continue;
|
|
371
387
|
if (participant.done) {
|
|
372
388
|
const done = driver.activityHeads({ actors: [participant.name], kinds: ['done'] }, { edge: 'newest', limit: 1 }).items[0];
|
|
373
|
-
|
|
374
|
-
|
|
389
|
+
if (done === undefined || index >= done.index)
|
|
390
|
+
continue;
|
|
375
391
|
const body = { channel: harness === 'claude' ? 'claude-code' : 'opencode', session: source.session,
|
|
376
392
|
...(source.agent === undefined ? {} : { agent: source.agent }) };
|
|
377
|
-
|
|
393
|
+
if (source.channel !== undefined && source.channel !== body.channel)
|
|
394
|
+
continue;
|
|
378
395
|
const coordinate = JSON.stringify([body.channel, body.session, body.agent]);
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
completedBodies.set(nameKey(participant.name), coordinate);
|
|
382
|
-
}
|
|
383
|
-
assertSource(source.kind === 'wake' || source.kind === 'presentation', 'delivery evidence kind is unsupported.');
|
|
384
|
-
assertSource(['dispatching', 'accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(source.outcome), 'delivery evidence outcome is unsupported.');
|
|
385
|
-
const at = sourceFinite(source.at);
|
|
386
|
-
const expiresAt = source.expiresAt === undefined ? undefined : sourceFinite(source.expiresAt);
|
|
387
|
-
const claimToken = source.claimToken;
|
|
388
|
-
assertSource(claimToken === undefined || typeof claimToken === 'string' && claimToken.length > 0, 'delivery claim token is malformed.');
|
|
389
|
-
if (claimToken !== undefined) {
|
|
390
|
-
assertSource(!tokens.has(claimToken), 'delivery claim token is ambiguous.');
|
|
391
|
-
tokens.add(claimToken);
|
|
396
|
+
const key = nameKey(participant.name), previous = completedBodies.get(key);
|
|
397
|
+
completedBodies.set(key, previous === undefined || previous === coordinate ? coordinate : null);
|
|
392
398
|
}
|
|
393
399
|
const group = JSON.stringify([nameKey(participant.name), generation, index, source.kind]);
|
|
394
400
|
const row = {
|
|
@@ -409,14 +415,19 @@ function planLegacyDeliveries(records, seats, driver) {
|
|
|
409
415
|
}
|
|
410
416
|
const deliveries = [];
|
|
411
417
|
for (const rows of byGroup.values()) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (explicit.length !== rows.length)
|
|
416
|
-
|
|
418
|
+
if (completedBodies.get(nameKey(rows[0].participant)) === null)
|
|
419
|
+
continue;
|
|
420
|
+
const explicit = rows.flatMap(row => row.attemptN === undefined ? [] : [row.attemptN]);
|
|
421
|
+
if (new Set(explicit).size !== explicit.length || explicit.length !== rows.length && rows.length !== 1)
|
|
422
|
+
continue;
|
|
417
423
|
rows.forEach((row, index) => deliveries.push({ ...row, attemptN: explicit.length === 0 ? 0 : explicit[index] }));
|
|
418
424
|
}
|
|
419
|
-
|
|
425
|
+
const tokens = new Map();
|
|
426
|
+
for (const row of deliveries) {
|
|
427
|
+
if (row.claimToken !== undefined)
|
|
428
|
+
tokens.set(row.claimToken, (tokens.get(row.claimToken) ?? 0) + 1);
|
|
429
|
+
}
|
|
430
|
+
return deliveries.filter(row => row.claimToken === undefined || tokens.get(row.claimToken) === 1);
|
|
420
431
|
}
|
|
421
432
|
const V4_REBUILT_TABLES = ['square_meta', 'catch_leases'];
|
|
422
433
|
function tableDefinition(name) {
|
package/guides/migration.md
CHANGED
|
@@ -38,25 +38,44 @@ artifact. Its latest owner row for each participant at this artifact is selected
|
|
|
38
38
|
by source `(effectiveAt, seq)` only: no wall clock, retention, expiry, or ownership
|
|
39
39
|
history reconstruction is involved.
|
|
40
40
|
|
|
41
|
-
- A
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
41
|
+
- A legacy owner Body (`channel`, `session`, optional `agent`) establishes a
|
|
42
|
+
standing seat only when it agrees with all of that name's local route Bodies
|
|
43
|
+
and the establishing last join is verified. An owner without routes needs no
|
|
44
|
+
wake address. Without a legacy owner, one unambiguous Body recorded by that
|
|
45
|
+
participant's own routes establishes its seat instead. An absent agent means
|
|
46
|
+
the top-level body; a known agent is preserved and compared exactly. Repeated
|
|
47
|
+
matching registrations are acceptable; a wake address is carried forward only
|
|
48
|
+
when the routes agree on it. Routes are location-bound: moving an artifact copy
|
|
49
|
+
to another path does not establish that its original routes apply there. An
|
|
50
|
+
off-location copy is not ownership-recovery evidence for the original location.
|
|
51
|
+
- Without an owner or any route Body, the participant stays standing without a
|
|
52
|
+
seat. Its source participant state, activities, attention, receipts, landed
|
|
53
|
+
audience and catch lease are preserved. No Body, route or departure is invented;
|
|
54
|
+
the name has no write authority or delivery eligibility. Doctor retains its
|
|
55
|
+
strict history-derived consistency checks.
|
|
56
|
+
- A Body may own several participant seats. Each name retains its own seat,
|
|
57
|
+
establishing join generation and wake address; sharing does not merge names or
|
|
58
|
+
choose one name over another. All evidence for each name must agree: if its
|
|
59
|
+
routes disagree with each other or with its legacy owner Body, that name stays
|
|
60
|
+
standing without a seat. Disagreement does not abort migration and neither side
|
|
61
|
+
is chosen as authoritative. Unverifiable joins still refuse before publication.
|
|
62
|
+
Migration does not guess or synthesize authority.
|
|
63
|
+
- Evidence structure is validated before placement, including rows that cannot be
|
|
64
|
+
imported. Malformed fields, invalid activity IDs or malformed native correlation
|
|
65
|
+
still refuse without changing either source.
|
|
66
|
+
- Wake/presentation evidence for a recovered seat is imported only when its Body
|
|
67
|
+
agrees and it names an existing directed say after the verified current join.
|
|
68
|
+
A missing seat, conflicting session/agent/channel or native harness, unknown
|
|
69
|
+
participant, or activity outside the generation causes the row to be skipped,
|
|
70
|
+
not a failed conversion. Nothing is reassigned to another owner.
|
|
54
71
|
- Done participants get **no seat or completed coordinate**. Only native wake
|
|
55
72
|
correlation directly attributable to their verified latest join and a directed
|
|
56
73
|
activity between that join and done is retained for retired-token recognition.
|
|
57
|
-
It does not authorize admission, completion, or presentation.
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
It does not authorize admission, completion, or presentation. Conflicting
|
|
75
|
+
completed Body mappings, duplicate claim tokens and ambiguous attempt numbering
|
|
76
|
+
cause all affected rows to be skipped, rather than selecting a winner. A single
|
|
77
|
+
record without an attempt number receives attempt 0. Migrated Claude native
|
|
78
|
+
addresses include `sessionId` on both seats and evidence routes.
|
|
60
79
|
|
|
61
80
|
This is deliberately a narrow, lossy conversion, not a general history importer.
|
|
62
81
|
Conversation facts (activity IDs, bodies and audience), receipts and source revision
|
|
@@ -65,15 +84,23 @@ correlation survive direct mappings. Completed non-native evidence and noncritic
|
|
|
65
84
|
metadata—including old presence history, epochs, diagnostic messages, native
|
|
66
85
|
observation histories and arbitrary legacy fields—are not imported. The legacy
|
|
67
86
|
ledger is read-only and remains untouched, so it is still available for offline
|
|
68
|
-
diagnostics. An absent ledger
|
|
69
|
-
or unsupported ledger refuses before changing the square.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
diagnostics. An absent ledger still permits route-derived seats; an unreadable
|
|
88
|
+
or unsupported ledger refuses before changing the square. Evidence without a
|
|
89
|
+
current or verified completed owner is skipped and counted. The source ledger is
|
|
90
|
+
retained for offline diagnosis; skipped evidence never grants delivery authority.
|
|
91
|
+
|
|
92
|
+
After successful preflight, one SQLite transaction imports seats and evidence
|
|
93
|
+
and adds delivery deferral support, then publishes the current format. All
|
|
94
|
+
existing activities, their indices and audience, participant state, attention,
|
|
95
|
+
receipts, lease contents, and the source revision are preserved. Legacy routes
|
|
96
|
+
are consumed only during migration. Lease generations are resolved only for exact
|
|
97
|
+
owner spellings that match a standing participant; other lease rows retain their
|
|
98
|
+
legacy, unbound generation. The command reports recovered seats and unowned
|
|
99
|
+
standing names, and recovered seats whose Body is shared with another recovered
|
|
100
|
+
seat, and skipped evidence rows, alongside the activity, receipt, lease and source
|
|
101
|
+
route counts. The shared count measures seats, not distinct Bodies or shared groups.
|
|
102
|
+
The skipped count includes every wake/presentation row read from the selected
|
|
103
|
+
legacy ledger but not imported, including completed non-native diagnostics.
|
|
77
104
|
The new delivery revision starts at zero. There is no intermediate published
|
|
78
105
|
format, and any failure before commit rolls back the conversion.
|
|
79
106
|
|
|
@@ -90,8 +117,7 @@ reader; migration consumes the old rows only to resolve seats, then drops the le
|
|
|
90
117
|
table as part of the format change.
|
|
91
118
|
|
|
92
119
|
Unrelated databases, unsupported formats, non-`DELETE` journal modes, unknown
|
|
93
|
-
schemas, malformed headers or
|
|
94
|
-
standing or native delivery evidence are refused. The source artifact is preserved on every
|
|
120
|
+
schemas, malformed headers, rows or native correlation are refused. The source artifact is preserved on every
|
|
95
121
|
refusal. Migration is not a repair command and never rewrites journal mode to make
|
|
96
122
|
conversion possible. A HostLedger that itself requires migration must be migrated
|
|
97
123
|
separately before Square migration can import its delivery evidence.
|
package/package.json
CHANGED