@astrosheep/square 0.3.71 → 0.3.73
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/artifact.d.ts +0 -3
- package/dist/artifact.js +5 -59
- package/dist/catch-decisions.d.ts +7 -1
- package/dist/catch-decisions.js +21 -8
- package/dist/claude-delivery.d.ts +1 -1
- package/dist/claude-delivery.js +7 -3
- package/dist/cli/ledger-command.d.ts +12 -0
- package/dist/cli/ledger-command.js +68 -0
- package/dist/cli/registry.js +2 -0
- package/dist/help.js +7 -2
- package/dist/host-ledger-file-adapter.d.ts +4 -10
- package/dist/host-ledger-file-adapter.js +238 -198
- package/dist/host-ledger-migration.d.ts +26 -0
- package/dist/host-ledger-migration.js +227 -0
- package/dist/host-ledger-sqlite.d.ts +62 -0
- package/dist/host-ledger-sqlite.js +341 -0
- package/dist/host-ledger.d.ts +13 -4
- package/dist/ledger-maintenance.d.ts +30 -0
- package/dist/ledger-maintenance.js +110 -0
- package/dist/packages/agent-delivery/src/claude.d.ts +2 -11
- package/dist/packages/agent-delivery/src/index.d.ts +2 -42
- package/dist/packages/agent-delivery/src/opencode.d.ts +2 -11
- package/dist/packages/agent-delivery/src/paseo.d.ts +2 -13
- package/dist/packages/agent-delivery/src/pi-client.d.ts +2 -9
- package/dist/packages/agent-delivery/src/types.d.ts +42 -0
- package/dist/participant-host.d.ts +6 -3
- package/dist/participant-host.js +44 -38
- package/dist/ports.d.ts +7 -1
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +1 -1
- package/dist/routes.js +1 -10
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.js +12 -6
- package/dist/sqlite-transaction.d.ts +5 -0
- package/dist/sqlite-transaction.js +72 -0
- package/dist/square-actions.js +26 -22
- package/dist/square-application.js +2 -14
- package/dist/square-migration.js +2 -1
- package/dist/square-sqlite.d.ts +2 -2
- package/dist/square-sqlite.js +15 -4
- package/guides/catch-contention-performance.md +157 -0
- package/guides/change-waits.md +1 -1
- package/guides/host-ledger.md +53 -6
- package/guides/indexed-host-ledger-performance.md +39 -0
- package/guides/postcommit-simplification-performance.md +36 -0
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/client/square-message.tsx +51 -40
- package/plugins/paseo/shared/message.ts +1 -0
- package/plugins/pi/square-pi.js +2 -1
package/dist/square-actions.js
CHANGED
|
@@ -2,32 +2,35 @@ import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume,
|
|
|
2
2
|
import { isSquareError, nameKey, SquareError, validateName } from './model.js';
|
|
3
3
|
import { participantIdentity } from './participant-identity.js';
|
|
4
4
|
import { deliverPending } from './delivery-operations.js';
|
|
5
|
-
import { decideCatch } from './catch-decisions.js';
|
|
5
|
+
import { decideCatch, observeCatchSelection, selectCatchPage } from './catch-decisions.js';
|
|
6
6
|
import { inSquareCount } from './runtime.js';
|
|
7
7
|
import { claimSessionParticipant, claimSessionTakeover, presenceEpoch, releaseSessionParticipantClaim, readParticipantOwner } from './registry.js';
|
|
8
|
-
import { assertLiveOwner, ensureLocalPresence, identityRouteDraft, processIdentity,
|
|
8
|
+
import { assertLiveOwner, ensureLocalPresence, identityRouteDraft, processIdentity, publishCommittedIdentity, retireIdentityRoute } from './participant-host.js';
|
|
9
9
|
import { applyWakeRoute, dropEndedSessionWakeRoutes, dropParticipantWakeRoutes, dropSessionWakeRoutes, sessionCanEndParticipant } from './routes.js';
|
|
10
10
|
import { parseRequiredActivityId, toPublicActivity } from './views.js';
|
|
11
|
-
/** Host enrichment is best effort after an authoritative artifact commit. */
|
|
12
|
-
async function publishCommittedPresence(square, name, epoch) {
|
|
13
|
-
await ensureLocalPresence(square, name, epoch).catch(() => undefined);
|
|
14
|
-
await publishIdentityRoute(square, name, epoch).catch(() => undefined);
|
|
15
|
-
}
|
|
16
11
|
function throwIfAborted(control) { if (control?.signal?.aborted)
|
|
17
12
|
throw control.signal.reason ?? new Error('Operation aborted'); }
|
|
18
13
|
export async function catchUp(square, name, options = {}, control, leaseId) {
|
|
19
14
|
throwIfAborted(control);
|
|
20
15
|
if (!await assertLiveOwner(square, name))
|
|
21
16
|
throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
}
|
|
17
|
+
let selected;
|
|
18
|
+
if (leaseId === undefined && options.id === undefined) {
|
|
19
|
+
selected = (await square.artifact.read((facts) => {
|
|
20
|
+
const page = selectCatchPage(facts, name, options);
|
|
21
|
+
return page.indexes.length === 0 ? observeCatchSelection(facts, page.participant, [], page.filter, square.clock()) : undefined;
|
|
22
|
+
}, control?.signal)).value;
|
|
23
|
+
}
|
|
24
|
+
if (selected === undefined)
|
|
25
|
+
selected = (await square.artifact.transact((facts) => {
|
|
26
|
+
// Replacement and selection share the receipt transaction: an old waiter cannot
|
|
27
|
+
// consume a later activity between its heartbeat and its selection.
|
|
28
|
+
if (leaseId !== undefined && facts.lease(name)?.leaseId !== leaseId)
|
|
29
|
+
return null;
|
|
30
|
+
return decideCatch(facts, name, options, square.clock());
|
|
31
|
+
}, control?.signal)).value;
|
|
29
32
|
if (selected !== null)
|
|
30
|
-
await
|
|
33
|
+
await publishCommittedIdentity(square, selected.participant);
|
|
31
34
|
return selected;
|
|
32
35
|
}
|
|
33
36
|
export async function join(square, name, control) {
|
|
@@ -80,7 +83,7 @@ export async function join(square, name, control) {
|
|
|
80
83
|
await releaseSessionParticipantClaim(hostLedger, ownershipClaim).catch(() => undefined);
|
|
81
84
|
throw error;
|
|
82
85
|
}
|
|
83
|
-
await
|
|
86
|
+
await publishCommittedIdentity(square, committed.name, epoch);
|
|
84
87
|
return {
|
|
85
88
|
name: committed.name, activity: committed.stored === null ? null : toPublicActivity(committed.stored),
|
|
86
89
|
participantCount: committed.participantCount, reconnected: false,
|
|
@@ -118,7 +121,7 @@ export async function takeover(square, name, _oldSessionIds = [], control) {
|
|
|
118
121
|
}, async (claim) => {
|
|
119
122
|
throwIfAborted(control);
|
|
120
123
|
const committed = await commitLifecycle();
|
|
121
|
-
await
|
|
124
|
+
await publishCommittedIdentity(square, committed.name, claim.epoch);
|
|
122
125
|
return committed;
|
|
123
126
|
}, control?.signal);
|
|
124
127
|
if (outcome.status === 'busy')
|
|
@@ -126,7 +129,7 @@ export async function takeover(square, name, _oldSessionIds = [], control) {
|
|
|
126
129
|
return { name: outcome.result.name, activities: outcome.result.stored.map(toPublicActivity), participantCount: outcome.result.participantCount, epoch: outcome.epoch };
|
|
127
130
|
}
|
|
128
131
|
const committed = await commitLifecycle();
|
|
129
|
-
await
|
|
132
|
+
await publishCommittedIdentity(square, committed.name);
|
|
130
133
|
return { name: committed.name, activities: committed.stored.map(toPublicActivity), participantCount: committed.participantCount };
|
|
131
134
|
}
|
|
132
135
|
export async function implicitJoin(square, name, control) {
|
|
@@ -147,11 +150,12 @@ export async function implicitJoin(square, name, control) {
|
|
|
147
150
|
}
|
|
148
151
|
return { name: decision.joinedName, state: decision.state, stored: decision.joinAct === undefined ? null : facts.land(decision.joinAct) };
|
|
149
152
|
}, control?.signal)).value;
|
|
150
|
-
|
|
153
|
+
if (committed.stored !== null)
|
|
154
|
+
await publishCommittedIdentity(square, committed.name);
|
|
155
|
+
else
|
|
156
|
+
await ensureLocalPresence(square, committed.name);
|
|
151
157
|
if (committed.state === 'done')
|
|
152
158
|
await retireIdentityRoute(square, committed.name);
|
|
153
|
-
else if (committed.stored !== null)
|
|
154
|
-
await publishIdentityRoute(square, committed.name);
|
|
155
159
|
return { name: committed.name, state: committed.state, activity: committed.stored === null ? null : toPublicActivity(committed.stored) };
|
|
156
160
|
}
|
|
157
161
|
function rejectUnsentActivity(operation) {
|
|
@@ -210,7 +214,7 @@ export async function express(square, name, body, options = {}, control) {
|
|
|
210
214
|
...(decision.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: decision.replyAuthorNotHere }),
|
|
211
215
|
};
|
|
212
216
|
}, control?.signal)).value;
|
|
213
|
-
await
|
|
217
|
+
await publishCommittedIdentity(square, name);
|
|
214
218
|
const delivery = square.wakeTransport !== undefined && square.hostLedger !== undefined && square.location !== undefined && square.location !== 'memory'
|
|
215
219
|
? await deliverPending({ artifact: square.artifact, hostLedger: square.hostLedger, transport: square.wakeTransport, location: square.location, activity: committed.stored.index, now })
|
|
216
220
|
.catch(() => ({ attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 }))
|
|
@@ -13,8 +13,7 @@ import { recoveryObservation, currentParticipant, entryPresentation, eventPresen
|
|
|
13
13
|
import { hasAutomaticDeliveryIdentity, hostLedgerForEnv, localParticipantName, localSessionIdentities, presenceEpoch, readParticipantOwner } from './registry.js';
|
|
14
14
|
import { projectSessionBindings } from './square-projections.js';
|
|
15
15
|
import { sessionIdsFromEnvironment } from './participant-identity.js';
|
|
16
|
-
import { createDefaultWakeTransport
|
|
17
|
-
import { sweepPending } from './delivery-operations.js';
|
|
16
|
+
import { createDefaultWakeTransport } from './notifications.js';
|
|
18
17
|
import { acquireWatchLease, ownsWatchLease, pulseWatchLease, releaseWatchLease } from './wakes.js';
|
|
19
18
|
import { createSquare, diagnoseSquareFile } from './artifact.js';
|
|
20
19
|
import { migrateSquareFile } from './square-migration.js';
|
|
@@ -187,7 +186,7 @@ function caughtResult({ selection, stop }) {
|
|
|
187
186
|
consumedThrough: selection.consumedThrough, remaining: selection.remaining, idleExpired: stop === 'stale',
|
|
188
187
|
};
|
|
189
188
|
}
|
|
190
|
-
/** Leases, quiet waiting
|
|
189
|
+
/** Leases, quiet waiting and selection form one lifecycle for both APIs. */
|
|
191
190
|
async function catchOwned(square, requested, options = {}, control) {
|
|
192
191
|
let name = requested, subject = 'caller';
|
|
193
192
|
let leaseId;
|
|
@@ -204,17 +203,6 @@ async function catchOwned(square, requested, options = {}, control) {
|
|
|
204
203
|
await releaseWatchLease(square, name, leaseId).catch(() => undefined);
|
|
205
204
|
leaseId = undefined;
|
|
206
205
|
}
|
|
207
|
-
if (!control?.signal?.aborted && stop !== 'replaced' && stop !== 'already-active'
|
|
208
|
-
&& (waitStarted || selected.activities.length > 0) && square.location !== 'memory'
|
|
209
|
-
&& square.hostLedger !== undefined && square.env?.SQUARE_DISABLE_PASEO_WAKE !== '1') {
|
|
210
|
-
try {
|
|
211
|
-
await sweepPending({
|
|
212
|
-
artifact: square.artifact, hostLedger: square.hostLedger, location: square.location,
|
|
213
|
-
now: square.clock(), graceMs: wakeGraceMs(square.env), limit: 8,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
catch { /* a completed receipt is not conditional on notification enrichment */ }
|
|
217
|
-
}
|
|
218
206
|
return { selection: selected, stop: terminal, waitStarted, replaced };
|
|
219
207
|
}
|
|
220
208
|
const empty = (stop) => complete(undefined, stop);
|
package/dist/square-migration.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { openExistingSquareDatabase } from './artifact.js';
|
|
2
|
+
import { inSqliteTransaction } from './sqlite-transaction.js';
|
|
2
3
|
import { canonicalPath } from './canonical-path.js';
|
|
3
4
|
import { isWakeRouteKind } from './model.js';
|
|
4
5
|
import { parseActivityId } from './square-core.js';
|
package/dist/square-sqlite.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type DatabaseSync } from 'node:sqlite';
|
|
2
2
|
import { SquareError, type ActivityObservation, type HardCap, type StoredAct, type WakeRoute, type WatchLease } from './model.js';
|
|
3
|
-
import type { SquareRead, RouteFact } from './ports.js';
|
|
3
|
+
import type { SquareHeader, SquareRead, RouteFact } from './ports.js';
|
|
4
4
|
import { type LandingWriter } from './square-domain.js';
|
|
5
5
|
/** Explicit import/export DTOs, not a runtime transaction model. */
|
|
6
6
|
export interface SquareRuntimeState {
|
|
@@ -43,7 +43,7 @@ export declare function invalidArtifact(detail: string): SquareError;
|
|
|
43
43
|
export declare function validateStoredActivity(value: unknown): StoredAct;
|
|
44
44
|
export declare function validateWatchLease(value: unknown): WatchLease;
|
|
45
45
|
export declare function validateRoute(value: unknown): RouteFact;
|
|
46
|
-
export declare function validateSqliteHeader(db: DatabaseSync, location?: string):
|
|
46
|
+
export declare function validateSqliteHeader(db: DatabaseSync, location?: string): SquareHeader;
|
|
47
47
|
export interface SqliteDriver extends SquareRead, LandingWriter {
|
|
48
48
|
readonly changed: boolean;
|
|
49
49
|
}
|
package/dist/square-sqlite.js
CHANGED
|
@@ -141,6 +141,7 @@ CREATE TABLE landed_audience (
|
|
|
141
141
|
UNIQUE (act_index, recipient_order)
|
|
142
142
|
) WITHOUT ROWID;
|
|
143
143
|
CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);`;
|
|
144
|
+
const EXPECTED_SCHEMA_SQL = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
|
|
144
145
|
const encode = JSON.stringify;
|
|
145
146
|
const key = (name) => encode(nameKey(name));
|
|
146
147
|
export function invalidArtifact(detail) {
|
|
@@ -330,11 +331,10 @@ export function validateSqliteHeader(db, location = '') {
|
|
|
330
331
|
throw new SquareError('invalid_args', `Square artifact needs migration.\n${migrationRecoveryCommand(location)}`, { reason: 'migration_required' });
|
|
331
332
|
}
|
|
332
333
|
requireFact(identity.user_version === 2, 'unsupported SQLite format version.');
|
|
333
|
-
const expected = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
|
|
334
334
|
const actual = db.prepare("SELECT sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' AND sql IS NOT NULL").all().map(r => normalizedSql(string(r.sql))).sort();
|
|
335
|
-
requireFact(actual.length ===
|
|
335
|
+
requireFact(actual.length === EXPECTED_SCHEMA_SQL.length && actual.every((sql, i) => sql === EXPECTED_SCHEMA_SQL[i]), 'unsupported row schema.');
|
|
336
336
|
requireFact(db.prepare('PRAGMA journal_mode').get().journal_mode === (location === ':memory:' ? 'memory' : 'delete'), 'unsupported journal mode.');
|
|
337
|
-
createSqliteDriver(db).header();
|
|
337
|
+
return createSqliteDriver(db).header();
|
|
338
338
|
}
|
|
339
339
|
function activitySelection(filter, alias = 'a') {
|
|
340
340
|
const terms = [], args = [];
|
|
@@ -418,6 +418,10 @@ export function createSqliteDriver(db) {
|
|
|
418
418
|
const r = one('SELECT * FROM participants WHERE name_key_json=?', [key(name)]);
|
|
419
419
|
return r ? participant(r) : undefined;
|
|
420
420
|
},
|
|
421
|
+
participantCounts() {
|
|
422
|
+
const r = one('SELECT COUNT(*) AS known, COALESCE(SUM(joined),0) AS joined, COALESCE(SUM(done),0) AS done FROM participants');
|
|
423
|
+
return immutable({ known: integer(r.known), joined: integer(r.joined), done: integer(r.done) });
|
|
424
|
+
},
|
|
421
425
|
roster: () => immutable(all('SELECT * FROM participants ORDER BY first_index').map(participant)),
|
|
422
426
|
attention: name => immutable(attentionRows('t.listener_key_json=?', [key(name)])),
|
|
423
427
|
incomingAttention: name => immutable(attentionRows('t.target_key_json=?', [key(name)])),
|
|
@@ -468,7 +472,14 @@ export function createSqliteDriver(db) {
|
|
|
468
472
|
sayOrdinals(indexes) {
|
|
469
473
|
indexes.forEach(v => integer(v));
|
|
470
474
|
const result = {};
|
|
471
|
-
|
|
475
|
+
// Only an actor's latest say can use its authoritative lifetime count.
|
|
476
|
+
for (const r of all(`SELECT a.act_index, CASE
|
|
477
|
+
WHEN p.say_count IS NOT NULL AND a.act_index=(SELECT MAX(s.act_index) FROM activities s WHERE s.kind='say' AND s.actor_key_json=a.actor_key_json)
|
|
478
|
+
THEN p.say_count
|
|
479
|
+
ELSE (SELECT COUNT(*) FROM activities s WHERE s.kind='say' AND s.actor_key_json=a.actor_key_json AND s.act_index<=a.act_index)
|
|
480
|
+
END AS n
|
|
481
|
+
FROM activities a LEFT JOIN participants p ON p.name_key_json=a.actor_key_json
|
|
482
|
+
WHERE a.kind='say' AND a.act_index IN (SELECT value FROM json_each(?))`, [encode(indexes)]))
|
|
472
483
|
result[integer(r.act_index)] = integer(r.n, 1);
|
|
473
484
|
return immutable(result);
|
|
474
485
|
},
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Catch contention: commit retries, empty reads, and shorter writes
|
|
2
|
+
|
|
3
|
+
A busy SQLite COMMIT retains the write transaction while waiting for readers to leave. Previously it slept 25 ms between attempts, the same as contenders waiting to acquire a transaction. The retained writer also prevents those contenders from progressing.
|
|
4
|
+
|
|
5
|
+
The change retries COMMIT after 1 ms while retaining the existing 25 ms acquisition backoff. It preserves the transaction, callback result, cancellation and rollback behavior. The shared helper applies to both artifact and host-ledger transactions. No journal mode, receipt semantics, workload, cache or background service changed.
|
|
6
|
+
|
|
7
|
+
## Measurement
|
|
8
|
+
|
|
9
|
+
The benchmark now times opening, recognition, first catch, second catch and closing separately. Both baseline and candidate used this instrumentation and the same command:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 300 --mode session --active --no-hints
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Three sequential runs per implementation, each covering both history sizes. Baseline production is `f44f359`; candidate changes only COMMIT retry timing. Baselines ran before candidates, not in randomized order. These local measurements show improvement but are not confidence intervals or a universal latency guarantee.
|
|
16
|
+
|
|
17
|
+
Values below are the **median of three run-level measurements**. For latency, each measurement is the p95 across 300 workers.
|
|
18
|
+
|
|
19
|
+
| Metric | Before, 1,800 history | After, 1,800 | Before, 18,000 | After, 18,000 |
|
|
20
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
21
|
+
| First catch API p95 (ms) | 6,596.63 | 4,619.84 | 7,302.29 | 4,781.38 |
|
|
22
|
+
| Second catch API p95 (ms) | 3,620.93 | 2,413.91 | 4,034.01 | 2,613.97 |
|
|
23
|
+
| Whole catch phase p95 (ms) | 7,262.52 | 5,464.51 | 8,126.17 | 5,653.63 |
|
|
24
|
+
| Aggregate phase CPU (ms) | 23,466.80 | 20,205.76 | 27,313.38 | 21,241.62 |
|
|
25
|
+
| Artifact busy observations | 47,578 | 30,824 | 53,418 | 32,265 |
|
|
26
|
+
| Artifact COMMIT busy observations | 95 | 138 | 85 | 117 |
|
|
27
|
+
|
|
28
|
+
First-catch p95 medians fell approximately 30% and 35%; whole-phase medians fell 25% and 30%. Aggregate CPU medians fell 14% and 22%. COMMIT retry counts increased, as expected from polling the retained writer sooner, while total busy observations and CPU medians decreased.
|
|
29
|
+
|
|
30
|
+
Run-level first-catch p95s, in execution order:
|
|
31
|
+
|
|
32
|
+
| History | Before (ms) | After (ms) |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| 1,800 | 6,119.17 / 6,596.63 / 6,680.30 | 4,619.84 / 4,233.23 / 6,642.77 |
|
|
35
|
+
| 18,000 | 7,785.32 / 7,302.29 / 5,966.01 | 4,781.38 / 4,995.59 / 4,694.29 |
|
|
36
|
+
|
|
37
|
+
The third small-history candidate run was slower than its other two runs and overlapped the baseline range. Its whole-phase p95 was 7,602.57 ms, above all three small-history baseline runs. Contention and machine-load variability remain; this change does not remove multi-second waiting under 300 simultaneous processes.
|
|
38
|
+
|
|
39
|
+
All twelve scenarios discovered 300 participants, consumed one activity each and consumed none again. All stderr files were empty. First-catch timing covers the full awaited API call, including ownership checks and postcommit identity publication. Whole-phase timing additionally includes open, recognize, the second catch and close; percentiles of individual steps must not be added.
|
|
40
|
+
|
|
41
|
+
Raw local reports: `/tmp/square-catch-baseline-{1,2,3}.json` and `/tmp/square-catch-commit-{1,2,3}.json`. These measure isolated local storage, not native delivery or ordinary single-user latency.
|
|
42
|
+
|
|
43
|
+
## Correctness boundaries
|
|
44
|
+
|
|
45
|
+
Existing SQLite lifecycle tests exercise real blocked COMMITs, single callback execution, event-loop progress, cancellation rollback and cross-process receipt exclusion. Cancellation tests cover precommit rollback and postcommit success. The COMMIT-only measurements above retain the empty-catch write transaction. The subsequent optimization below measures its removal separately.
|
|
46
|
+
|
|
47
|
+
## Follow-up: empty catches and repeated observation work
|
|
48
|
+
|
|
49
|
+
Ordinary non-ID, non-leased catches now inspect pending indexes in a read transaction. An empty page returns its complete observation from that snapshot without acquiring the artifact writer or hydrating bodies. A nonempty page is selected again inside the existing receipt transaction; the probe never decides which activities get consumed. Exact-ID and leased catches keep their existing write path. Ownership checks and identity publication remain in place.
|
|
50
|
+
|
|
51
|
+
Catch observations now reuse one roster and header for standing count and terminal status. Schema validation precomputes only the expected SQL constant; actual schema, identity, version, journal mode, and header checks still run inside each transaction. These three changes were measured together, so the results do not isolate each change's contribution.
|
|
52
|
+
|
|
53
|
+
The same benchmark command ran three times on baseline `03623cf`, then three times on the candidate, covering both histories each time. These are fresh baselines, separate from the earlier COMMIT experiment. No workload ran alongside the benchmarks. As above, ordering was sequential, not randomized, and results describe this local 300-process workload.
|
|
54
|
+
|
|
55
|
+
| Metric | Before, 1,800 history | After, 1,800 | Before, 18,000 | After, 18,000 |
|
|
56
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
57
|
+
| First catch API p95 (ms) | 5,681.03 | 3,729.06 | 4,967.29 | 3,654.90 |
|
|
58
|
+
| Second, empty catch API p95 (ms) | 2,868.83 | 79.80 | 2,608.11 | 55.08 |
|
|
59
|
+
| Whole catch phase p95 (ms) | 6,315.96 | 4,111.88 | 5,701.10 | 3,887.74 |
|
|
60
|
+
| Aggregate phase CPU (ms) | 22,441.95 | 17,401.83 | 21,456.88 | 16,184.47 |
|
|
61
|
+
| Artifact busy observations | 34,542 | 18,780 | 32,986 | 18,638 |
|
|
62
|
+
| Artifact COMMIT busy observations | 137 | 323 | 148 | 287 |
|
|
63
|
+
|
|
64
|
+
Each cell is the median of three run-level measurements. Empty-catch p95 medians fell 97% and 98%; first-catch medians fell 34% and 26%; whole-phase medians fell 35% and 32%; CPU medians fell 22% and 25%. Total busy observations fell, while COMMIT busy observations increased. The extra read probe changes contention; fewer writer acquisitions do not imply every busy counter falls.
|
|
65
|
+
|
|
66
|
+
Run-level p95s in execution order:
|
|
67
|
+
|
|
68
|
+
| History | Call | Before (ms) | After (ms) |
|
|
69
|
+
| --- | --- | --- | --- |
|
|
70
|
+
| 1,800 | First | 7,655.42 / 4,857.49 / 5,681.03 | 4,274.17 / 3,729.06 / 3,557.04 |
|
|
71
|
+
| 18,000 | First | 4,967.29 / 5,029.40 / 4,895.89 | 5,005.24 / 3,356.44 / 3,654.90 |
|
|
72
|
+
| 1,800 | Empty | 4,495.46 / 2,348.03 / 2,868.83 | 56.35 / 79.80 / 82.83 |
|
|
73
|
+
| 18,000 | Empty | 3,139.28 / 2,506.64 / 2,608.11 | 53.31 / 55.08 / 56.97 |
|
|
74
|
+
|
|
75
|
+
The first large-history candidate's consuming catch overlaps the baseline range. Consuming catches still wait seconds under this load; this is not a guarantee of millisecond catch latency generally. The second catch measures an empty result immediately after consumption, not a long-running idle lease.
|
|
76
|
+
|
|
77
|
+
All twelve scenarios discovered 300 participants, consumed exactly one activity per worker, and consumed none again: 3,600 successful worker results. Each scenario hydrated 300 bodies and issued 600 artifact SQL writes. Empty baseline transactions already wrote no rows; the improvement removes writer acquisition, not receipt writes. Successful artifact transactions per scenario increase from 1,800 to 2,100 because nonempty catches add a read probe. All stderr files were empty.
|
|
78
|
+
|
|
79
|
+
Focused tests exercise empty snapshot completeness, absence of writer acquisition/body hydration/receipts, a competing consumer between probe and write selection, exact-ID repetition, lease fencing, validation, cancellation, and ownership publication. The existing performance assertion now checks that an empty catch makes no hydration request.
|
|
80
|
+
|
|
81
|
+
Raw local reports: `/tmp/square-catch-empty-before-{1,2,3}.json` and `/tmp/square-catch-empty-after-{1,2,3}.json`. The final cap-first short circuit in `watchTerminalStatus()` restores its earlier read behavior; it is used by lease pulses and lies outside this benchmark's non-idle path.
|
|
82
|
+
|
|
83
|
+
## Follow-up: shortening consuming transactions
|
|
84
|
+
|
|
85
|
+
The next candidate adds transaction-local participant counts instead of decoding the whole roster for catch/terminal summaries. Latest-say ordinals use the existing participant lifetime count when the selected activity is that actor's latest say; older activities retain the indexed prefix count. Artifact acquisition also passes its already-validated header to the transaction callback instead of reading and decoding it again. Schema validation still occurs inside every transaction. There is no persistent schema change or cross-transaction cache.
|
|
86
|
+
|
|
87
|
+
The benchmark now measures explicit transaction boundaries per connection and separates artifact from host-ledger connections:
|
|
88
|
+
|
|
89
|
+
- Acquisition: first BEGIN attempt to successful BEGIN return, including retry gaps.
|
|
90
|
+
- Hold: successful BEGIN return to first COMMIT attempt, including validation, decoding and off-CPU wall time.
|
|
91
|
+
- Commit: first COMMIT attempt to successful COMMIT return, including retries.
|
|
92
|
+
|
|
93
|
+
Only committed transactions contribute timing samples. Rollback resets the sample; unsuccessful read acquisition/validation retries are visible in counts but do not become committed timings. `BEGIN DEFERRED` returning does not imply a read lock has already been acquired. Concurrent read durations cannot be summed into a serial lock-occupancy number. Batched transaction controls without observable inner boundaries are counted separately and excluded from exact timings. Artifact SQL execution durations exclude preparation and JavaScript decoding. Bounded self-tests exercise busy BEGIN, busy COMMIT, rollback/reset and origin separation.
|
|
94
|
+
|
|
95
|
+
Fresh baseline `13d1a36` and candidate each ran three times with the same new instrumentation and the command above, both histories per run. Baselines ran first; no test suite or second benchmark ran alongside the measurements. These changes were measured together, not as independent experiments.
|
|
96
|
+
|
|
97
|
+
| Metric | Before, 1,800 history | After, 1,800 | Before, 18,000 | After, 18,000 |
|
|
98
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
99
|
+
| First catch API p95 (ms) | 3,528.76 | 2,678.86 | 3,685.07 | 2,865.71 |
|
|
100
|
+
| Empty catch API p95 (ms) | 68.23 | 89.97 | 58.31 | 91.11 |
|
|
101
|
+
| Whole catch phase p95 (ms) | 3,831.22 | 2,998.22 | 4,165.47 | 3,177.27 |
|
|
102
|
+
| Write acquisition p95 (ms) | 2,739.46 | 1,950.57 | 2,961.04 | 2,257.01 |
|
|
103
|
+
| Write hold p95 (ms) | 8.33 | 5.32 | 10.05 | 5.88 |
|
|
104
|
+
| Write commit p95 (ms) | 4.15 | 3.81 | 4.36 | 4.24 |
|
|
105
|
+
| Aggregate phase CPU (ms) | 16,348.86 | 13,014.67 | 17,732.24 | 13,499.75 |
|
|
106
|
+
| Artifact busy observations | 16,201 | 13,081 | 20,023 | 12,508 |
|
|
107
|
+
|
|
108
|
+
Cells are medians of three run-level measurements. Each scenario has 300 successful consuming write transactions, so write percentiles span those 300 transactions. First-catch p95 medians fall 24% and 22%; whole-phase medians fall 22% and 24%; CPU falls 20% and 24%. Write acquisition falls 29% and 24%, and hold p95 falls 36% and 42%. Percentiles of these stages must not be added to reconstruct the API percentile.
|
|
109
|
+
|
|
110
|
+
Empty-catch p95 regresses by 22 ms and 33 ms (32% and 56%) in this contended workload. The faster consuming path does not improve every operation. The current measurements do not isolate the cause of this regression; empty catches still use a read transaction, and the 25 ms acquisition retry interval is unchanged. The consuming-call improvement outweighs it in whole-phase time, but the regression remains part of the result.
|
|
111
|
+
|
|
112
|
+
| History | Call | Before p95 runs (ms) | After p95 runs (ms) |
|
|
113
|
+
| --- | --- | --- | --- |
|
|
114
|
+
| 1,800 | First | 3,572.79 / 3,123.90 / 3,528.76 | 2,361.69 / 2,732.76 / 2,678.86 |
|
|
115
|
+
| 18,000 | First | 3,577.88 / 3,685.07 / 4,165.72 | 2,865.71 / 2,975.57 / 2,813.31 |
|
|
116
|
+
| 1,800 | Empty | 68.23 / 85.81 / 61.32 | 100.68 / 89.97 / 87.47 |
|
|
117
|
+
| 18,000 | Empty | 57.38 / 59.12 / 58.31 | 108.55 / 83.11 / 91.11 |
|
|
118
|
+
|
|
119
|
+
Every candidate first-catch p95 is below its history's baseline range, but these sequential local runs are not confidence intervals or a general latency guarantee. Acquisition still takes seconds near the tail under 300 simultaneous processes. Some individual hold/commit samples are much longer than their p95; wall-time instrumentation alone cannot attribute all of that time to SQL or process scheduling.
|
|
120
|
+
|
|
121
|
+
All twelve scenarios discovered 300 participants, consumed one activity each, and consumed none again: 3,600 successful worker results. Each scenario retains 300 hydrated bodies, 600 SQL writes, 300 committed artifact writes and 1,800 committed artifact reads. All stderr files were empty. Tests cover aggregate read-your-writes and rollback, known-but-not-standing peers, missing/done caller quorum, cap precedence, and ordinal numbering across aliases, gaps, rejoins and subsequent writes.
|
|
122
|
+
|
|
123
|
+
Raw local reports: `/tmp/square-catch-positive-before-{1,2,3}.json` and `/tmp/square-catch-positive-after-{1,2,3}.json`. Earlier reports lack these transaction timing fields and should not be mixed into this comparison.
|
|
124
|
+
|
|
125
|
+
## Follow-up: spreading writer acquisition retries
|
|
126
|
+
|
|
127
|
+
Writer acquisition now uses a per-transaction backoff window of 25, 50, then at most 100 ms. Each busy retry independently chooses an integer delay from 1 through that window. Read acquisition and other default retries remain fixed at 25 ms; a busy COMMIT still retries after 1 ms while retaining the writer. The shared helper applies this policy to artifact and host-ledger writes. Cancellation, rollback, synchronous callbacks, receipt selection and storage framing are unchanged.
|
|
128
|
+
|
|
129
|
+
This changes both retry frequency and synchronization, not jitter alone. At the cap the mean requested delay is 50.5 ms rather than 25 ms. The intent is to reduce contender overhead, not to pretend queue waiting is idle time: serial write hold plus commit already accounts for most of the previous catch duration. Wall-time measurements cannot prove how much of the benefit comes from scheduling or filesystem contention. There is no fairness guarantee; an individual contended retry can sleep longer than before. Uncontended attempts do not sleep.
|
|
130
|
+
|
|
131
|
+
Baseline `f004621` and the isolated writer-only candidate each ran three times with the same instrumentation and command above. Execution order was baseline 1, three rejected schema-normalization experiments, baselines 2–3, one broader retry pilot, then writer-only candidates 1–3. No build, test suite or second benchmark ran alongside a benchmark. This was not a randomized comparison; prior tables are separate experiments and should not be used as its baseline. The schema-normalization fast path showed no consistent benefit and was removed. The broader pilot is excluded from the results below.
|
|
132
|
+
|
|
133
|
+
| Metric | Before, 1,800 history | After, 1,800 | Before, 18,000 | After, 18,000 |
|
|
134
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
135
|
+
| First catch API p95 (ms) | 2,942.43 | 2,696.24 | 3,314.33 | 2,703.95 |
|
|
136
|
+
| Empty catch API p95 (ms) | 114.52 | 83.35 | 107.99 | 85.06 |
|
|
137
|
+
| Whole catch phase p95 (ms) | 3,281.04 | 3,089.50 | 3,797.85 | 3,008.00 |
|
|
138
|
+
| Write acquisition p95 (ms) | 2,460.38 | 2,044.42 | 2,441.14 | 2,114.18 |
|
|
139
|
+
| Write hold p95 (ms) | 6.43 | 5.46 | 7.20 | 5.39 |
|
|
140
|
+
| Write commit p95 (ms) | 4.72 | 3.87 | 4.64 | 3.50 |
|
|
141
|
+
| Aggregate phase CPU (ms) | 14,791.25 | 12,797.97 | 16,050.74 | 12,994.48 |
|
|
142
|
+
| Artifact busy observations | 14,311 | 8,943 | 15,203 | 8,966 |
|
|
143
|
+
|
|
144
|
+
Cells are medians of three run-level measurements. First-catch p95 medians improve 8% and 18%, whole-phase medians 6% and 21%, and CPU 13% and 19%. Empty-catch p95 medians improve 27% and 21%; total busy observations fall 38% and 41%. Median run-level maximum write acquisition falls from 2,835.81 to 2,626.85 ms and from 3,064.84 to 2,656.63 ms; this is not a bounded maximum or a starvation guarantee.
|
|
145
|
+
|
|
146
|
+
| History | Call | Before p95 runs (ms) | After p95 runs (ms) |
|
|
147
|
+
| --- | --- | --- | --- |
|
|
148
|
+
| 1,800 | First | 3,518.32 / 2,942.43 / 2,797.10 | 2,479.32 / 2,696.24 / 2,829.63 |
|
|
149
|
+
| 18,000 | First | 2,791.81 / 3,314.33 / 3,457.89 | 2,576.67 / 2,759.20 / 2,703.95 |
|
|
150
|
+
| 1,800 | Empty | 114.52 / 114.76 / 100.27 | 83.35 / 84.59 / 82.22 |
|
|
151
|
+
| 18,000 | Empty | 118.99 / 107.99 / 63.28 | 108.44 / 85.06 / 61.79 |
|
|
152
|
+
|
|
153
|
+
Small-history consuming and whole-phase ranges overlap; large-history consuming ranges do not. Not every tail metric improves: the large-history median of each run's maximum COMMIT duration rises from 506.88 to 920.92 ms, despite the lower COMMIT p95. These local three-run results show lower contention overhead, not a universal latency improvement. Consuming catch still takes seconds near the tail with 300 simultaneous processes.
|
|
154
|
+
|
|
155
|
+
All twelve scenarios discovered 300 participants, consumed exactly one activity each and consumed none again: 3,600 successful worker results. Each retains 300 hydrated bodies, 600 SQL writes, 300 committed artifact writes and 1,800 committed artifact reads. All stderr files were empty. A new real-lock test checks cancellation before writer acquisition and successful single-callback execution on a subsequent transaction; existing tests retain busy COMMIT cancellation, same-result retries and cross-process receipt exclusion.
|
|
156
|
+
|
|
157
|
+
Raw local reports: `/tmp/square-lock-before-{1,2,3}.json` and `/tmp/square-lock-writer-{1,2,3}.json`. Discarded experiments are `/tmp/square-lock-schema-{1,2,3}.json` and `/tmp/square-lock-jitter-1.json`.
|
package/guides/change-waits.md
CHANGED
|
@@ -4,7 +4,7 @@ Square's artifact and host-ledger adapters own change detection. Notifications a
|
|
|
4
4
|
|
|
5
5
|
- A process shares a revision reader per canonical artifact and a filesystem watcher per containing directory. Subscribers have independent baselines, deadlines, and cancellation. The final release closes the watcher, reconciliation timer, and any outstanding read.
|
|
6
6
|
- Directory notifications invalidate lightweight reads. A two-second reconciliation checks revisions/ledger metadata even when notifications are missing. Missing directories, unsupported watches, and watcher failures retain that fallback; directory replacement is detected during reconciliation. Normal notification latency is not a hard guarantee: a missed hint may wait for reconciliation, plus scheduling and lock contention.
|
|
7
|
-
- The artifact revision remains authoritative. No new sidecar, broker, or persistent cache is introduced. Changed projections read typed recipient facts and selected bodies, not the whole historical state. A missing artifact is a stable observation, not a repeated change: reconciliation remains quiet while it stays absent and detects its return without deleting its session bindings. Strict artifact reads still report absence as an error.
|
|
7
|
+
- The artifact revision remains authoritative. No new sidecar, broker, or persistent cache is introduced. Changed projections read typed recipient facts and selected bodies, not the whole historical state. A missing artifact is a stable observation, not a repeated change: reconciliation remains quiet while it stays absent and detects its return without deleting its session bindings. Strict artifact reads still report absence as an error. Host-ledger database metadata invalidates a process-shared read of its separate presence/evidence revisions; unchanged metadata requires no SQLite open. Revisions are read together in one short snapshot and cached against metadata captured before that read, so a concurrent mutation remains detectable. Failed reads are not cached. Missing ledger storage is stable until it appears. Neither metadata nor revisions are delivery evidence.
|
|
8
8
|
- Each Pi session owns a long-lived pending observer, including when it has no bindings yet. Presence and evidence changes update its subscriptions. There is no repeated thirty-second restart and no per-second binding scan. A failed injection waits for a substantive change; its own failed evidence write does not trigger a retry loop.
|
|
9
9
|
- Blocking CLI catch waits for a revision or its next heartbeat/quiet deadline. A held catch keeps its lease alive and pauses quiet expiry. Core catch, NDJSON stream, and delivery-receipt waits use the same change detector. Held express also waits for changes; throttling still waits for its eligibility time.
|
|
10
10
|
- Catch lease expiry, ledger retention/claim expiry, and cancellation are independent wake conditions. No filesystem write is required for a time-based eligibility change.
|
package/guides/host-ledger.md
CHANGED
|
@@ -1,11 +1,58 @@
|
|
|
1
|
-
# Host
|
|
1
|
+
# Host ledger
|
|
2
2
|
|
|
3
|
-
Square uses one user-level host
|
|
3
|
+
Square uses one user-level `host-ledger.sqlite` for participant bindings, delivery evidence, and wake attempts. It is separate from every `.square` artifact: the ledger owns host identity, ownership epochs, discovery and delivery evidence; the artifact owns square activity and participant lifecycle.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The default root is `~/.square/host-ledger`. Tests and isolated harnesses may set `SQUARE_HOST_LEDGER_ROOT`; otherwise `SQUARE_REGISTRY` selects its containing directory. An explicit adapter `rootPath` takes precedence. These settings select a single root, not additional ledgers.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Storage and delivery
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
SQLite uses DELETE journaling and FULL synchronous writes. Records retain original order and duplicates, with derived lookup indexes and separate presence/evidence revisions. Normal reads of an empty root create nothing. First mutation initializes storage only when neither legacy NDJSON file exists. Legacy-only storage refuses ordinary operation and requires explicit offline conversion; normal operation never dual-reads or automatically migrates.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
One wake attempt is one evidence record. Acquisition creates `dispatching` directly, before a send may happen. An interrupted dispatch becomes `unknown`, which forbids redispatch; current token-fenced recovery may still accept a late native admission. Accepted and possibly-sent wake records remain readable at any age. Prepared Claude correlations also bypass age visibility and housekeeping across every outcome, including failed and released, until explicitly retired. Ordinary records retain their existing seven-day visibility horizon, with expired ordinary rows reclaimed only during actual mutations. There is no automatic maintenance GC or activity-id-only sweep; durable delivery evidence is retired only by the explicit, artifact-proven `square ledger gc` below.
|
|
12
|
+
|
|
13
|
+
## Verify and convert offline
|
|
14
|
+
|
|
15
|
+
These commands need no participant or square location and use the resolved ledger root:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
square ledger verify
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Verification is read-only. It checks SQLite identity, version, schema, integrity, record codecs, derived indexes and revisions. With only legacy files, it scans both complete sources and reports accepted, ignored and rejected counts, readiness, and SHA-256 hashes of the original source bytes. Unsafe indexed records make readiness false without hiding the remaining counts or hashes; the CLI reports not ready and exits with status 2. With no files, it reports empty without creating a directory, database or lock. Output never includes record payloads, native credentials or endpoints.
|
|
22
|
+
|
|
23
|
+
To upgrade an NDJSON ledger:
|
|
24
|
+
|
|
25
|
+
1. Stop **every** Square host using that ledger, including interactive harnesses and background agents. Do not run old and new hosts together.
|
|
26
|
+
2. Keep a backup of the stopped legacy files.
|
|
27
|
+
3. Run:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
square ledger verify
|
|
31
|
+
square ledger migrate
|
|
32
|
+
square ledger verify
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
4. Start all hosts with the updated Square version together, using the same root.
|
|
36
|
+
|
|
37
|
+
Migration does not stop or restart hosts. Its locks fence compatible writers but do not prove hosts are stopped. It acquires the ownership claim fence, then `presence.ndjsonl.lock`, then `evidence.ndjsonl.lock`.
|
|
38
|
+
|
|
39
|
+
An older SQLite ledger format is upgraded in place by the same `square ledger migrate` under the same stopped-hosts prerequisite and the same lock order. The upgrade preserves every record, duplicate, original order and both observer revisions. `square ledger verify` stays read-only: it refuses an older or unsupported database with the migrate command and never upgrades silently.
|
|
40
|
+
|
|
41
|
+
The importer preserves all ages, future records, duplicates, original order, native correlation and residual fields. It ignores malformed JSON, wrong versions and records without a numeric timestamp, as the old reader did, and reports those counts. Any valid version-1 record whose indexed fields are unsafe refuses the entire conversion rather than losing evidence. Refusal identifies only source kind, rejected count and first rejected line; no accepted subset is published. Legacy unknown wake sources are normalized once during import.
|
|
42
|
+
|
|
43
|
+
A temporary SQLite database is built and verified under the same root, then published without overwriting an existing destination. Legacy sources remain byte-for-byte untouched. A valid existing SQLite ledger is an idempotent no-op; an older SQLite ledger format is upgraded in place under the fence; invalid or unsupported destinations are refused without repair. There is no force option. On a reported failure, keep hosts stopped, resolve the cause and rerun verification/conversion. An abrupt process termination can leave an unpublished `.host-ledger-migrate-*` directory; after confirming no converter is running, it can be removed without touching the sources or published database.
|
|
44
|
+
|
|
45
|
+
Once SQLite exists, legacy files are not consulted. Keeping them does not provide a rollback path after new SQLite writes: reverting to them would lose ownership and possibly-sent evidence. For a live SQLite ledger, use SQLite backup facilities rather than copying only its main file.
|
|
46
|
+
|
|
47
|
+
## Reclaim old evidence
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
square ledger gc --dry-run
|
|
51
|
+
square ledger gc
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`gc` considers wake and presentation evidence at least seven days old whose attention is permanently closed, proven per row against the square artifact named in the row: the activity historically reached that participant, and either catch recorded an exact receipt or the participant's later rejoin moved past it. Rows still `dispatching`, lifecycle rows, rows with unusable coordinates and rows whose artifact is missing, invalid or unreadable are retained. Eligibility is a proof, not a promise: `retired` counts rows actually removed, and a row whose record changed after the proof, or whose Claude correlation cannot be retired safely, is retained instead. `--dry-run` proves the same set and changes neither the ledger nor any artifact.
|
|
55
|
+
|
|
56
|
+
Reclamation is counted in rows, not bytes. Deleted rows free storage for reuse but do not necessarily shrink the SQLite file immediately; `gc` never runs `VACUUM`. Retiring a prepared Claude correlation payload leaves only a permanent `(harness, session, token)` recognition record, without payload, endpoint, location, participant, epoch or timestamp. These records are absent from ordinary evidence and attempts and confer no admission, presentation or receipt authority. Any retired token vetoes the entire native frame, including a mixed live/retired frame. Total token count remains unbounded. There are no hooks, timers or install steps: maintenance runs only when asked.
|
|
57
|
+
|
|
58
|
+
The obsolete `SQUARE_HOST_LEDGER_USER` and `SQUARE_HOST_LEDGER_LOCAL` settings, project-local fallback and scope-based copying are not used. Older local files and `wake-claims.ndjsonl` remain untouched and are not imported. Bindings that existed only in those abandoned stores require their host to rejoin; do not discard the supported ledger to reset bindings.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Indexed host-ledger measurement
|
|
2
|
+
|
|
3
|
+
The indexed host ledger eliminates whole-file NDJSON reads. This run does not show a consistent reduction in the 300-process catch-phase latency: the smaller-history case was slightly slower and the larger-history case was faster. Artifact contention remains substantial.
|
|
4
|
+
|
|
5
|
+
## Comparable workload
|
|
6
|
+
|
|
7
|
+
Baseline: `895ff50` (NDJSON with origin-separated instrumentation). SQLite plus safe retirement candidate: `60ea8b0`. Both used the same benchmark script, 300 processes, session discovery, active bell, disabled filesystem hints, and histories of 1,800 and 18,000 unrelated activities.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 300 --mode session --active --no-hints
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The catch phase times opening the square, recognizing the participant, a first catch, a second catch checking duplicate consumption, and closing the square. These numbers are **not single-catch latency**, nor ordinary single-user latency. Discovery is measured separately. These are one completed run per implementation and history size, not repeated-run confidence intervals.
|
|
14
|
+
|
|
15
|
+
| Metric | NDJSON, 1,800 | SQLite, 1,800 | NDJSON, 18,000 | SQLite, 18,000 |
|
|
16
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
17
|
+
| Catch-phase p50 (ms) | 6,134.67 | 6,159.17 | 6,775.98 | 5,532.52 |
|
|
18
|
+
| Catch-phase p95 (ms) | 7,307.72 | 7,481.01 | 8,182.00 | 7,170.30 |
|
|
19
|
+
| Catch-phase aggregate CPU (ms) | 24,171.34 | 24,045.67 | 27,538.51 | 23,574.19 |
|
|
20
|
+
| Artifact busy observations | 46,710 | 52,686 | 56,401 | 46,069 |
|
|
21
|
+
| Catch-phase artifact body rows | 300 | 300 | 300 | 300 |
|
|
22
|
+
| Catch-phase artifact writes | 600 | 600 | 600 | 600 |
|
|
23
|
+
| Discovery p95 (ms) | 1,931 | 1,927 | 1,881 | 1,915 |
|
|
24
|
+
|
|
25
|
+
Every completed scenario discovered all 300 participants, consumed exactly one activity per participant, and consumed none again on the second catch.
|
|
26
|
+
|
|
27
|
+
## Ledger work
|
|
28
|
+
|
|
29
|
+
For either history size, the NDJSON catch phase performed 1,500 file reads totaling 95,520,000 bytes. The SQLite catch phase performed 1,500 data-read executions, returning 1,500 presence rows; all SQL result values together totaled 316,900 bytes (4,500 rows including header results). Host-ledger SQL busy observations were zero.
|
|
30
|
+
|
|
31
|
+
These are different transport measurements: file bytes versus SQL values returned to JavaScript. They demonstrate that whole-ledger materialization is gone; they do **not** measure equivalent physical disk I/O or prove a proportional speedup. SQLite bypasses the JavaScript filesystem hooks, so zero NDJSON operations/bytes is structural, not a standalone performance result.
|
|
32
|
+
|
|
33
|
+
The remaining catch-phase contention is attributed to the square artifact, separately from the host ledger. The result motivates investigating concurrent artifact transactions if reducing this latency is the next objective; it does not establish the benefit of any unimplemented change.
|
|
34
|
+
|
|
35
|
+
## Evidence and limits
|
|
36
|
+
|
|
37
|
+
Local raw reports: `/tmp/square-indexed-baseline-300.json` and `/tmp/square-indexed-final-300.json`. The completed SQLite run exited zero with empty stderr. An earlier invocation was terminated by the tool's 60-second execution limit before producing a report and was excluded; the completed run used the unchanged workload in a background process.
|
|
38
|
+
|
|
39
|
+
This benchmark uses an isolated ledger. It does not migrate the live host ledger, exercise real native transports, or measure GC reclamation. GC correctness is covered by the separate retirement, closure-proof, schema-upgrade, and CLI tests. Retirement leaves reusable SQLite pages and indefinitely retained Claude recognition tokens; neither immediate file shrinkage nor bounded total token growth is promised.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Postcommit simplification: 300-process comparison
|
|
2
|
+
|
|
3
|
+
Measured on the same local host with Node v26.10.0. Baseline runtime: release 0.3.71 (`90478f8`). Candidate runtime: `03237aa`. The benchmark entrypoint repair in `2bfb68e` was used for both; it replaces imports of removed modules with current artifact/open-square exports, without changing the scenario or deadlines.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm run build
|
|
7
|
+
node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 300 --mode session --active --no-hints
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The command exercises both 1,800 and 18,000 unrelated historical activities. Each of 300 processes observes one bell, recognizes its participant, performs one consuming catch and one empty repeat. These are synthetic Pi bindings without callable native routes. Counts below aggregate all 300 processes; catch latency describes that entire per-process catch phase, not one standalone API call. MB means decimal bytes. Ledger bytes measure instrumented file reads/writes, not physical disk traffic.
|
|
11
|
+
|
|
12
|
+
| Measurement | Baseline, 1,800 | Candidate, 1,800 | Baseline, 18,000 | Candidate, 18,000 |
|
|
13
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
14
|
+
| Ready / complete | 300 / 300 | 300 / 300 | 300 / 300 | 300 / 300 |
|
|
15
|
+
| Caught per process | 1 | 1 | 1 | 1 |
|
|
16
|
+
| Reconsumed total | 0 | 0 | 0 | 0 |
|
|
17
|
+
| Catch ledger MB | 229.248 | 95.520 | 229.248 | 95.520 |
|
|
18
|
+
| Catch ledger operations | 4,200 | 1,500 | 4,200 | 1,500 |
|
|
19
|
+
| Catch p50 seconds | 18.65 | 5.80 | 22.76 | 5.36 |
|
|
20
|
+
| Catch p95 seconds | 22.57 | 7.11 | 26.62 | 6.85 |
|
|
21
|
+
| Catch SQLite busy errors/retries | 174,082 | 45,555 | 221,701 | 45,028 |
|
|
22
|
+
| Catch CPU seconds, aggregate | 65.01 | 23.31 | 73.50 | 21.65 |
|
|
23
|
+
| Discovery p95 seconds | 1.870 | 1.877 | 1.877 | 1.897 |
|
|
24
|
+
| Five-second idle CPU seconds, aggregate | 3.181 | 3.241 | 3.594 | 3.088 |
|
|
25
|
+
|
|
26
|
+
These historical SQL/busy counts combined artifact SQL with host-ledger file-lock SQL; “busy errors/retries” counted thrown busy errors, not independently observed retries. They cannot be compared directly with the origin-separated counters now in the benchmark. Rerun both runtimes with the same new script. Its `ledgerSql` reports host-root SQL (including lock databases), separately from artifact SQL. Returned SQL string/blob bytes are not physical I/O or full JSON decode measurements; SQLite's zero instrumented filesystem bytes is a transport change, not a performance gain.
|
|
27
|
+
|
|
28
|
+
Idle body hydration and ledger bytes remained zero. Each discovery and catch phase hydrated 300 activity bodies. The original 20-second discovery deadline and derived phase guard were unchanged.
|
|
29
|
+
|
|
30
|
+
The candidate removes a discarded catch-completion sweep and combines postcommit identity publication around one fresh presence observation. Admission remains a separate ownership check. Source changes also consolidate duplicate native connection-option declarations; those type changes do not account for the performance result. Production TypeScript across the three changes is net -57 lines without formatting compression. The benchmark repair is separately net -1 tooling line.
|
|
31
|
+
|
|
32
|
+
Ledger bytes fell 58.3%, and ledger operation count fell 64.3%. The observed latency improvement is a single before/after local comparison, not an SLA or an isolated causal attribution: other agent work and verification occurred during the broader run, and host load was not controlled. The deterministic operation reduction supports the intended deletion of duplicate work. Idle discovery was not redesigned, and this does not measure callable-route publication or long-lived leased catch optimization.
|
|
33
|
+
|
|
34
|
+
Each change passed independent review and its declared verification before integration. Root CLI process checks passed all 48 cases; the native adapter package passed 42 cases with its existing Windows-only skip. Full root verification retained its existing skipped case.
|
|
35
|
+
|
|
36
|
+
Local raw reports: `/tmp/square-round2-baseline-300.json` and `/tmp/square-round2-final-300.json`. These temporary reports are diagnostic artifacts, not repository dependencies. No ledger storage migration or evidence GC change is included.
|
package/package.json
CHANGED