@astrosheep/square 0.3.73 → 0.3.75
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/attention-presentation.d.ts +2 -0
- package/dist/attention-presentation.js +11 -2
- package/dist/boundary-presentation.d.ts +8 -1
- package/dist/boundary-presentation.js +8 -1
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/express-presentation.js +1 -2
- package/dist/cli/ledger-command.js +6 -2
- package/dist/cli/meta-commands.js +1 -1
- package/dist/decisions.js +6 -21
- package/dist/help.js +4 -4
- package/dist/host-ledger-migration.d.ts +1 -0
- package/dist/host-ledger-migration.js +62 -21
- package/dist/host-ledger-sqlite.d.ts +4 -1
- package/dist/host-ledger-sqlite.js +36 -9
- package/dist/inbox.d.ts +2 -0
- package/dist/inbox.js +64 -22
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +51 -26
- package/dist/model.d.ts +2 -3
- package/dist/model.js +5 -2
- package/dist/open-square.d.ts +12 -2
- package/dist/open-square.js +42 -4
- package/dist/participant-host.js +6 -2
- package/dist/ports.d.ts +10 -0
- package/dist/presentation.js +1 -1
- package/dist/square-actions.d.ts +3 -1
- package/dist/square-actions.js +10 -5
- package/dist/square-application.d.ts +4 -0
- package/dist/square-application.js +42 -28
- package/dist/square-facade.d.ts +26 -22
- package/dist/views.d.ts +3 -1
- package/dist/views.js +37 -29
- package/guides/application-demand-optimization.md +50 -0
- package/guides/catch-cold-path-optimization.md +56 -0
- package/guides/catch-lock-investigation.md +81 -0
- package/guides/host-ledger.md +2 -2
- package/guides/mcp-connection-lifetime.md +22 -0
- package/guides/pi-boundary-pending-reuse.md +13 -0
- package/package.json +2 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/README.md +10 -5
- package/plugins/paseo/client/markdown.tsx +221 -0
- package/plugins/paseo/client/square-message.tsx +64 -94
- package/plugins/paseo/client/transform.ts +4 -1
- package/plugins/paseo/shared/message.ts +6 -4
- package/plugins/pi/square-pi.js +26 -16
- package/skills/square/SKILL.md +1 -1
package/dist/square-facade.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ActivityId, Perception, Reach } from './square-core.js';
|
|
2
2
|
import type { StoredAct } from './model.js';
|
|
3
|
-
import type { HostLedgerPort, WakeTransportPort, DeliveryResult } from './ports.js';
|
|
3
|
+
import type { HostLedgerPort, WakeTransportPort, DeliveryResult, SquareResourceOwner } from './ports.js';
|
|
4
4
|
import type { DiagnoseResult } from './artifact.js';
|
|
5
5
|
import type { MigrationOutcome } from './square-migration.js';
|
|
6
6
|
export type { ActivityId, Perception, Reach } from './square-core.js';
|
|
@@ -136,10 +136,14 @@ export interface SquareApplicationContext extends Readonly<OpenOptions> {
|
|
|
136
136
|
readonly env: NodeJS.ProcessEnv;
|
|
137
137
|
readonly squarePath?: string;
|
|
138
138
|
readonly participant?: string;
|
|
139
|
+
/** Borrow the owner's connection for the configured square. The caller owns its lifetime;
|
|
140
|
+
* without an owner, each operation opens and closes its own connection. */
|
|
141
|
+
readonly resources?: SquareResourceOwner;
|
|
139
142
|
}
|
|
140
|
-
|
|
143
|
+
/** Data-only callers do not request or pay for presentation work. */
|
|
144
|
+
export interface ApplicationOutcome<Data, Projection, Presentation extends boolean = true> {
|
|
141
145
|
readonly data: Data;
|
|
142
|
-
readonly projection: Projection;
|
|
146
|
+
readonly projection: Presentation extends true ? Projection : undefined;
|
|
143
147
|
}
|
|
144
148
|
export interface ApplicationFailureDetail {
|
|
145
149
|
readonly operation: 'express' | 'join' | 'catch' | 'history' | 'other';
|
|
@@ -444,26 +448,26 @@ export interface StatusProjection extends SquareContextProjection, PresenceClass
|
|
|
444
448
|
}
|
|
445
449
|
export interface ParticipantsProjection extends SquareContextProjection, PresenceClassification {
|
|
446
450
|
}
|
|
447
|
-
export interface SquareApplication {
|
|
451
|
+
export interface SquareApplication<Presentation extends boolean = true> {
|
|
448
452
|
now(): number;
|
|
449
|
-
build(input: BuildApplicationInput, control?: OperationControl): Promise<ApplicationOutcome<BuildApplicationData, SquareContextProjection>>;
|
|
450
|
-
join(options?: JoinApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<JoinApplicationData, EntryProjection>>;
|
|
451
|
-
express(body: string, options?: ExpressApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<ExpressApplicationData, ExpressProjection>>;
|
|
452
|
-
catch(options?: CatchApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<CatchApplicationData, CatchProjection>>;
|
|
453
|
-
history(query?: HistoryApplicationQuery, control?: OperationControl): Promise<ApplicationOutcome<HistoryApplicationData, HistoryProjection>>;
|
|
454
|
-
listen(target: string, control?: OperationControl): Promise<ApplicationOutcome<ListenerChangeData, ListenerProjection>>;
|
|
455
|
-
ignore(target: string, control?: OperationControl): Promise<ApplicationOutcome<ListenerChangeData, ListenerProjection>>;
|
|
456
|
-
listening(control?: OperationControl): Promise<ApplicationOutcome<ListeningData, SquareContextProjection>>;
|
|
457
|
-
hold(reason?: string, control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection>>;
|
|
458
|
-
resume(control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection>>;
|
|
459
|
-
done(body?: string, control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection>>;
|
|
460
|
-
status(viewer?: string, control?: OperationControl): Promise<ApplicationOutcome<StatusApplicationData, StatusProjection>>;
|
|
461
|
-
participants(control?: OperationControl): Promise<ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection>>;
|
|
462
|
-
diagnose(control?: OperationControl): Promise<ApplicationOutcome<DiagnosisApplicationData, Partial<SquareContextProjection
|
|
463
|
-
migrate(control?: OperationControl): Promise<ApplicationOutcome<MigrationApplicationData, Partial<SquareContextProjection
|
|
464
|
-
discover(options: DiscoveryApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<DiscoveryApplicationData, DiscoveryProjection>>;
|
|
465
|
-
inbox(sessionId: string, options?: InboxApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<InboxApplicationData, InboxProjection>>;
|
|
466
|
-
recovery(control?: OperationControl): Promise<ApplicationOutcome<RecoveryApplicationData, RecoveryProjection>>;
|
|
453
|
+
build(input: BuildApplicationInput, control?: OperationControl): Promise<ApplicationOutcome<BuildApplicationData, SquareContextProjection, Presentation>>;
|
|
454
|
+
join(options?: JoinApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<JoinApplicationData, EntryProjection, Presentation>>;
|
|
455
|
+
express(body: string, options?: ExpressApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<ExpressApplicationData, ExpressProjection, Presentation>>;
|
|
456
|
+
catch(options?: CatchApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<CatchApplicationData, CatchProjection, Presentation>>;
|
|
457
|
+
history(query?: HistoryApplicationQuery, control?: OperationControl): Promise<ApplicationOutcome<HistoryApplicationData, HistoryProjection, Presentation>>;
|
|
458
|
+
listen(target: string, control?: OperationControl): Promise<ApplicationOutcome<ListenerChangeData, ListenerProjection, Presentation>>;
|
|
459
|
+
ignore(target: string, control?: OperationControl): Promise<ApplicationOutcome<ListenerChangeData, ListenerProjection, Presentation>>;
|
|
460
|
+
listening(control?: OperationControl): Promise<ApplicationOutcome<ListeningData, SquareContextProjection, Presentation>>;
|
|
461
|
+
hold(reason?: string, control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection, Presentation>>;
|
|
462
|
+
resume(control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection, Presentation>>;
|
|
463
|
+
done(body?: string, control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection, Presentation>>;
|
|
464
|
+
status(viewer?: string, control?: OperationControl): Promise<ApplicationOutcome<StatusApplicationData, StatusProjection, Presentation>>;
|
|
465
|
+
participants(control?: OperationControl): Promise<ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection, Presentation>>;
|
|
466
|
+
diagnose(control?: OperationControl): Promise<ApplicationOutcome<DiagnosisApplicationData, Partial<SquareContextProjection>, Presentation>>;
|
|
467
|
+
migrate(control?: OperationControl): Promise<ApplicationOutcome<MigrationApplicationData, Partial<SquareContextProjection>, Presentation>>;
|
|
468
|
+
discover(options: DiscoveryApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<DiscoveryApplicationData, DiscoveryProjection, Presentation>>;
|
|
469
|
+
inbox(sessionId: string, options?: InboxApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<InboxApplicationData, InboxProjection, Presentation>>;
|
|
470
|
+
recovery(control?: OperationControl): Promise<ApplicationOutcome<RecoveryApplicationData, RecoveryProjection, Presentation>>;
|
|
467
471
|
stream(options: StreamApplicationOptions, control?: OperationControl): AsyncIterable<StreamApplicationRecord>;
|
|
468
472
|
}
|
|
469
473
|
export type JoinApplicationResult = JoinApplicationData;
|
package/dist/views.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare const NON_READ: readonly ["join", "done", "say", "hold", "resume"
|
|
|
11
11
|
export declare function toPublicActivity(stored: StoredAct): Activity;
|
|
12
12
|
export declare function parseRequiredActivityId(id: ActivityId): number;
|
|
13
13
|
/** Compile the public query directly to typed selectors; only search hydrates unselected bodies. */
|
|
14
|
-
export declare function selectHistory(facts: SquareRead, query: HistoryApplicationQuery, limit: number | null, kinds: ActivityFilter['kinds']): {
|
|
14
|
+
export declare function selectHistory(facts: SquareRead, query: HistoryApplicationQuery, limit: number | null, kinds: ActivityFilter['kinds'], presentation?: boolean): {
|
|
15
15
|
continuation?: HistoryContinuation | undefined;
|
|
16
16
|
activities: StoredAct[];
|
|
17
17
|
visibleActivities: StoredAct[];
|
|
@@ -29,8 +29,10 @@ export declare function recoveryObservation(square: OpenSquare, name: string, co
|
|
|
29
29
|
export declare function entryPresentation(square: OpenSquare, name: string, lastN?: number | null, control?: OperationControl): Promise<EntryPresentation>;
|
|
30
30
|
type ParticipantQueryContext = Pick<SquareContextProjection, 'location' | 'now' | 'automaticDeliveryAvailable'>;
|
|
31
31
|
export declare function queryParticipants(facts: SquareRead, context: ParticipantQueryContext): ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection>;
|
|
32
|
+
export declare function queryParticipants(facts: SquareRead, context: ParticipantQueryContext, presentation: boolean): ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection, boolean>;
|
|
32
33
|
export declare function listPresentation(square: OpenSquare, control?: OperationControl): Promise<ListPresentation>;
|
|
33
34
|
export declare function queryStatus(facts: SquareRead, context: ParticipantQueryContext, viewer?: string): ApplicationOutcome<StatusApplicationData, StatusProjection>;
|
|
35
|
+
export declare function queryStatus(facts: SquareRead, context: ParticipantQueryContext, viewer: string, presentation: boolean): ApplicationOutcome<StatusApplicationData, StatusProjection, boolean>;
|
|
34
36
|
export declare function eventPresentation(square: OpenSquare, id: ActivityId, control?: OperationControl): Promise<EventPresentation>;
|
|
35
37
|
export declare function catchPresence(square: OpenSquare, control?: OperationControl): Promise<CatchPresence>;
|
|
36
38
|
export declare function streamProjection(square: OpenSquare, cursor: number, recipient?: string, control?: OperationControl): Promise<StreamProjection>;
|
package/dist/views.js
CHANGED
|
@@ -28,7 +28,7 @@ export function parseRequiredActivityId(id) {
|
|
|
28
28
|
return index;
|
|
29
29
|
}
|
|
30
30
|
/** Compile the public query directly to typed selectors; only search hydrates unselected bodies. */
|
|
31
|
-
export function selectHistory(facts, query, limit, kinds) {
|
|
31
|
+
export function selectHistory(facts, query, limit, kinds, presentation = true) {
|
|
32
32
|
if (query.before !== undefined && query.after !== undefined)
|
|
33
33
|
throw new SquareError('invalid_args', 'History cannot combine before and after cursors');
|
|
34
34
|
const afterIndex = query.after === undefined ? undefined : parseRequiredActivityId(query.after);
|
|
@@ -44,14 +44,16 @@ export function selectHistory(facts, query, limit, kinds) {
|
|
|
44
44
|
...(query.at?.length ? { indexes: facts.contextIndexes(query.at.map(parseRequiredActivityId), query.contextBefore ?? 0, query.contextAfter ?? 0) } : {}),
|
|
45
45
|
kinds: search === undefined ? kinds : ['say', 'done'],
|
|
46
46
|
};
|
|
47
|
-
let selected, totalMatches, publicBefore, publicAfter;
|
|
47
|
+
let selected, totalMatches, publicBefore = false, publicAfter = false;
|
|
48
48
|
if (recipient === undefined && search === undefined && limit !== null && limit <= PAGE_SIZE) {
|
|
49
49
|
selected = facts.activityHeads(filter, { edge: afterIndex === undefined ? 'newest' : 'oldest', limit }).items;
|
|
50
|
-
const publicHeads = selected.filter((head) => ARCHIVE.has(head.kind));
|
|
51
|
-
const publicFilter = { ...filter, kinds: [...ARCHIVE] };
|
|
52
50
|
totalMatches = facts.activityCount(filter);
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
if (presentation) {
|
|
52
|
+
const publicHeads = selected.filter((head) => ARCHIVE.has(head.kind));
|
|
53
|
+
const publicFilter = { ...filter, kinds: [...ARCHIVE] };
|
|
54
|
+
publicBefore = publicHeads.length > 0 && facts.activityCount({ ...publicFilter, beforeIndex: publicHeads[0].index }) > 0;
|
|
55
|
+
publicAfter = publicHeads.length > 0 && facts.activityCount({ ...publicFilter, afterIndex: publicHeads.at(-1).index }) > 0;
|
|
56
|
+
}
|
|
55
57
|
}
|
|
56
58
|
else {
|
|
57
59
|
const page = [];
|
|
@@ -68,7 +70,7 @@ export function selectHistory(facts, query, limit, kinds) {
|
|
|
68
70
|
if (matches !== undefined && !matches.has(head.index))
|
|
69
71
|
continue;
|
|
70
72
|
totalMatches++;
|
|
71
|
-
if (ARCHIVE.has(head.kind)) {
|
|
73
|
+
if (presentation && ARCHIVE.has(head.kind)) {
|
|
72
74
|
firstPublic = Math.min(firstPublic, head.index);
|
|
73
75
|
lastPublic = head.index;
|
|
74
76
|
}
|
|
@@ -82,14 +84,16 @@ export function selectHistory(facts, query, limit, kinds) {
|
|
|
82
84
|
cursor = source.items.at(-1).index;
|
|
83
85
|
}
|
|
84
86
|
selected = page;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
if (presentation) {
|
|
88
|
+
const publicHeads = selected.filter((head) => ARCHIVE.has(head.kind));
|
|
89
|
+
publicBefore = publicHeads.length > 0 && firstPublic < publicHeads[0].index;
|
|
90
|
+
publicAfter = publicHeads.length > 0 && lastPublic > publicHeads.at(-1).index;
|
|
91
|
+
}
|
|
88
92
|
}
|
|
89
93
|
const activities = [...facts.activities(selected.map((head) => head.index))];
|
|
90
94
|
if (query.order === 'desc')
|
|
91
95
|
activities.reverse();
|
|
92
|
-
const visibleActivities = activities.filter((activity) => ARCHIVE.has(activity.kind));
|
|
96
|
+
const visibleActivities = presentation ? activities.filter((activity) => ARCHIVE.has(activity.kind)) : [];
|
|
93
97
|
const direction = afterIndex === undefined ? 'before' : 'after';
|
|
94
98
|
const cursor = visibleActivities.length === 0 ? undefined : direction === 'before'
|
|
95
99
|
? Math.min(...visibleActivities.map((activity) => activity.index)) : Math.max(...visibleActivities.map((activity) => activity.index));
|
|
@@ -156,9 +160,9 @@ function classifyPresence(participants, now) {
|
|
|
156
160
|
classificationByName: Object.fromEntries(participants.map((participant) => [participant.name, participant.state === 'done' ? 'done'
|
|
157
161
|
: participant.state === 'not joined' ? 'not-joined' : hereNames.has(participant.name) ? 'here' : 'lingering'])) };
|
|
158
162
|
}
|
|
159
|
-
export function queryParticipants(facts, context) {
|
|
163
|
+
export function queryParticipants(facts, context, presentation = true) {
|
|
160
164
|
const participants = coreParticipants(facts, context.now);
|
|
161
|
-
return { data: { participants }, projection: { ...context, ...classifyPresence(participants, context.now),
|
|
165
|
+
return { data: { participants }, projection: !presentation ? undefined : { ...context, ...classifyPresence(participants, context.now),
|
|
162
166
|
standingCount: inSquareCount(facts), held: facts.hold().active } };
|
|
163
167
|
}
|
|
164
168
|
export async function listPresentation(square, control) {
|
|
@@ -170,30 +174,34 @@ export async function listPresentation(square, control) {
|
|
|
170
174
|
return { context: facts.header().preamble, participants, activities: facts.activityCount({ kinds: ['say'] }) };
|
|
171
175
|
}, control?.signal)).value;
|
|
172
176
|
}
|
|
173
|
-
export function queryStatus(facts, context, viewer = '') {
|
|
174
|
-
const
|
|
175
|
-
const header = facts.header(), hold = facts.hold();
|
|
177
|
+
export function queryStatus(facts, context, viewer = '', presentation = true) {
|
|
178
|
+
const participants = coreParticipants(facts, context.now);
|
|
179
|
+
const header = facts.header(), hold = facts.hold(), standingCount = inSquareCount(facts);
|
|
176
180
|
const latest = facts.activityHeads({ kinds: ['say', 'done'] }, { edge: 'newest', limit: 1 }).items[0];
|
|
177
181
|
const latestAct = latest === undefined ? undefined : facts.activity(latest.index);
|
|
182
|
+
const data = {
|
|
183
|
+
hardCap: header.hardCap,
|
|
184
|
+
...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
|
|
185
|
+
activeCount: standingCount, doneCount: participants.filter((p) => p.state === 'done').length,
|
|
186
|
+
hold: hold.active ? {
|
|
187
|
+
...(hold.actor === undefined ? {} : { by: hold.actor }),
|
|
188
|
+
...(hold.reason === undefined ? {} : { reason: hold.reason }),
|
|
189
|
+
...(hold.at === undefined ? {} : { at: hold.at }),
|
|
190
|
+
} : null,
|
|
191
|
+
participants: participants.filter((p) => p.state !== 'not joined'), now: context.now,
|
|
192
|
+
...(latestAct === undefined || latestAct.actor === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
|
|
193
|
+
};
|
|
194
|
+
if (!presentation)
|
|
195
|
+
return { data, projection: undefined };
|
|
178
196
|
const afterIndex = latestAct?.index ?? -1;
|
|
179
197
|
const joins = facts.activities(facts.activityHeads({ kinds: ['join'], afterIndex }, { edge: 'newest', limit: 3 }).items.map((h) => h.index)).map((activity) => ({ activity,
|
|
180
198
|
rejoined: facts.activityCount({ kinds: ['done'], actors: [activity.actor], actorMatch: 'exact', beforeIndex: activity.index }) > 0 }));
|
|
181
199
|
const canonicalViewer = facts.participant(viewer)?.name ?? viewer;
|
|
182
200
|
return {
|
|
183
|
-
data
|
|
184
|
-
hardCap: header.hardCap,
|
|
185
|
-
...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
|
|
186
|
-
activeCount: projection.standingCount, doneCount: participants.filter((p) => p.state === 'done').length,
|
|
187
|
-
hold: hold.active ? {
|
|
188
|
-
...(hold.actor === undefined ? {} : { by: hold.actor }),
|
|
189
|
-
...(hold.reason === undefined ? {} : { reason: hold.reason }),
|
|
190
|
-
...(hold.at === undefined ? {} : { at: hold.at }),
|
|
191
|
-
} : null,
|
|
192
|
-
participants: participants.filter((p) => p.state !== 'not joined'), now: context.now,
|
|
193
|
-
...(latestAct === undefined || latestAct.actor === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
|
|
194
|
-
},
|
|
201
|
+
data,
|
|
195
202
|
projection: {
|
|
196
|
-
...
|
|
203
|
+
...context, ...classifyPresence(participants, context.now), standingCount, held: hold.active,
|
|
204
|
+
knownParticipantCount: participants.length, viewer: canonicalViewer,
|
|
197
205
|
...(canonicalViewer === '' ? {} : { participant: canonicalViewer }),
|
|
198
206
|
joins, joinCount: facts.activityCount({ kinds: ['join'], afterIndex }),
|
|
199
207
|
attentionNames: participants.filter((p) => canonicalViewer === '' || nameKey(p.name) === nameKey(canonicalViewer)).map((p) => p.name),
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Application presentation demand
|
|
2
|
+
|
|
3
|
+
## Design
|
|
4
|
+
|
|
5
|
+
`createSquareApplication(context)` retains complete terminal-facing outcomes. MCP uses `createSquareApplication(context, { presentation: false })`; its outcome has the same `data` contract and `projection: undefined`.
|
|
6
|
+
|
|
7
|
+
This is one explicit result demand, not a second business implementation. The application still owns admission, retries, receipts, cancellation and delivery. Ordinary early returns and conditional expressions skip presentation work where it was previously performed; there is no deferred projector, registry, cache or resource-lifetime change. CLI projection types remain required, while the data-only factory overload exposes their absence explicitly.
|
|
8
|
+
|
|
9
|
+
MCP no longer requests join history/perceptions, the pre-expression directed feed, catch presence enrichment, listener context reads, lifecycle event rereads, or history/status/participants presentation-only annotations. Selection/counts required for results and history limits remain. For terminal expressions the directed feed is still captured once **before** landing, not after sending or on each retry. Delivery and postcommit publication remain on the original path.
|
|
10
|
+
|
|
11
|
+
Two same-operation observations are consolidated:
|
|
12
|
+
|
|
13
|
+
- Catch resolves caller and all sender filters in one typed-fact read, preserving caller-first validation, canonical error subjects, filter order and known-but-departed senders. The actual selection/receipt transaction, ownership admission and lease fences remain separate and current.
|
|
14
|
+
- Expression's known-name check runs inside the existing join preview via an internal `requireKnown` admission option. Unknown names still fail before claiming or mutating; departed participants still reenter once. Ordinary join may still create a name. Preview facts never replace the subsequent ownership/CAS or commit-time checks.
|
|
15
|
+
|
|
16
|
+
Long-lived MCP connections and concurrent delivery are deliberately outside this change.
|
|
17
|
+
|
|
18
|
+
## Local MCP measurement
|
|
19
|
+
|
|
20
|
+
Baseline: `31cc1bf`. Candidate: the presentation-demand and observation changes above. Node v26.10.0. A real MCP client used the SDK's in-memory transport, not a direct call to application helpers. Each call still creates its application and opens/closes the artifact.
|
|
21
|
+
|
|
22
|
+
Three runs per variant, alternating B1/A1/B2/A2/B3/A3. Each process seeds four participants and 120 2,048-byte historical says outside measurement, joins the probe participant, then runs three warmup cycles and twenty measured cycles of repeated join, express, two empty filtered catches, history and status. Harness session keys are cleared: the probe's `reconnect` label means a repeated no-op join (`kind: joined`), not a real harness ownership reconnect. New measured expressions are small (`probe body`) and use force without pending directed feed; catches are empty. This is not a bulk-payload, ownership-contention or concurrent benchmark. No test suite, build or second benchmark ran alongside the six probes.
|
|
23
|
+
|
|
24
|
+
Wall times below are the median of three run-level p50s, each from twenty measured calls. Small local timings are illustrative, not a production SLA or the earlier 300-process catch-p95 workload. The stable transaction/statement counts establish removed work more directly.
|
|
25
|
+
|
|
26
|
+
| MCP operation | Wall p50 before → after (ms) | Artifact read transactions/call | SQL prepares/call |
|
|
27
|
+
| --- | ---: | ---: | ---: |
|
|
28
|
+
| Repeated no-op join | 1.478 → 1.036 | 3 → 2 | 30 → 15 |
|
|
29
|
+
| Express | 3.963 → 3.372 | 6 → 4 | 79 → 61 |
|
|
30
|
+
| Empty catch, one sender filter | 3.157 → 1.613 | 5 → 3 | 124 → 40 |
|
|
31
|
+
| Empty catch, three sender filters | 3.335 → 1.655 | 7 → 3 | 134 → 42 |
|
|
32
|
+
| History | 1.582 → 0.808 | 2 → 2 | 52 → 16 |
|
|
33
|
+
| Status | 2.344 → 2.093 | 2 → 2 | 103 → 94 |
|
|
34
|
+
|
|
35
|
+
Sender lookups still scale with filter count; **transactions do not**. No participant validation has been omitted to obtain that result. Repeated join's body-hydrating row reads fall from ten to zero per call. History still hydrates its ten result rows, and both empty catches hydrate none. Express and status each retain one body-hydrating row in this probe; not all body reads are presentation work.
|
|
36
|
+
|
|
37
|
+
SQLite counters are attributed by canonical database location. Host-ledger/lock database work is counted separately, never attributed to the artifact. A positive calibration observes one port read as one artifact read transaction and `activities([0,1])` as two hydrated rows. History moves the same hydration counters used for the zero checks. Hydrated bytes in the probe mean returned string/blob bytes, not filesystem I/O.
|
|
38
|
+
|
|
39
|
+
All six probes passed their calibration, first-join, hydration and no-error controls: 720 measured calls plus six first joins have no recorded errors. Warmup results are not retained or included in that gate. Raw per-call records retain timing/counters/errors but not complete returned payloads; each operation also retains its first measured result summary. Public-data/error parity is established by regression tests, not by those no-error benchmark controls. Recorded compiled-module hashes were checked before and after each probe; the probe script itself was not included in those historical hash manifests. Independent review recomputed the raw timing/counter statistics and confirmed the stated measurement limits.
|
|
40
|
+
|
|
41
|
+
## Regression evidence
|
|
42
|
+
|
|
43
|
+
- New tests prove MCP avoids the extra presentation reads, while terminal-facing join/express still hydrate real presentation content and express retains its pre-send feed.
|
|
44
|
+
- A body-bearing catch hydrates exactly its selected page; empty catch and archive/status/participants data parity remain intact.
|
|
45
|
+
- One versus multiple catch filters has the same read-transaction count; canonical caller/filter errors remain distinct.
|
|
46
|
+
- Both new tests fail against the unchanged baseline at the intended hydration/transaction assertions.
|
|
47
|
+
- The existing CLI/application/MCP activity/receipt parity, never-joined refusal, departed reentry, metadata ownership, cancellation and postcommit behavior tests pass.
|
|
48
|
+
- Full suite: 680 passed, one opt-in Claude runtime test skipped. CLI process suite: 48 passed. Independent static review accepted correctness and control-flow clarity.
|
|
49
|
+
|
|
50
|
+
Local evidence: `/tmp/square-upper-{before,after}-{1,2,3}.{json,log,exit,sha,integrity}`. Probe: `/tmp/square-mcp-demand-bench.mjs`. Test logs: `/tmp/square-upper-{tests,cli,negative}.log`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Catch cold-path optimization
|
|
2
|
+
|
|
3
|
+
## Result and scope
|
|
4
|
+
|
|
5
|
+
Under the existing 300-process Pi-session workload, first-catch API p95 falls by **60.4% / 61.4%** for histories of 1,800 / 18,000 activities. These are reductions between the medians of three run-level p95s, not a guarantee for every run or operation. Whole catch-phase p95 falls by about **56%** for both histories.
|
|
6
|
+
|
|
7
|
+
The comparison is against `90752ba`, the current production implementation plus the same instrumentation, not an older, slower implementation. The benchmark represents Pi sessions without a Paseo route; it does not establish equal gains for Codex/Paseo sessions, emoji-only names, or native notification delivery.
|
|
8
|
+
|
|
9
|
+
## Two changes
|
|
10
|
+
|
|
11
|
+
- `model.ts`: try the existing Unicode text-name grammar before the full RGI emoji grammar. It accepts letters, numbers, marks excluding variation selectors, hyphens, underscores, and nonempty slash-separated segments. It is an exact subset of the original grammar; the original full matcher remains the fallback. `NAME_GRAPHEME_SOURCE`, also used by mention scanning, retains its original value.
|
|
12
|
+
- `participant-host.ts`: before discovering installed adapters, use the existing route resolver to check whether any candidate exists. No host ledger or no possible candidate means no route, without importing adapters that cannot be used. Real candidates still pass through the real capability check. Paseo precedence, Codex fallback, harness identity, and publication failure isolation remain unchanged.
|
|
13
|
+
|
|
14
|
+
CPU sampling of the baseline pointed to cold name validation inside reader transactions and optional adapter module loading during catch. The changes remove that work rather than prewarm it or move it outside measurement. The sampling and this combined experiment do not establish each change's individual contribution.
|
|
15
|
+
|
|
16
|
+
SQLite storage, DELETE journaling, FULL synchronous durability, transaction validation, ownership/epoch fences, receipts, and retry policy are unchanged. Earlier lock/backoff/cache experiments did not show the required improvement and are not included. The earlier [COMMIT-gap investigation](catch-lock-investigation.md) measured preparation inside writers; it did not bound costs inside concurrent readers or optional postcommit module loading.
|
|
17
|
+
|
|
18
|
+
## Controlled comparison
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 300 --mode session --active --no-hints
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Node v26.10.0. Three runs of each variant, ordered B1, A1, B2, A2, B3, A3. Each command covers both histories. Baseline worktree: `/tmp/square-half-baseline-final`, detached at `90752ba`; candidate: the main worktree with the two changes above. Benchmark scripts were byte-identical. The changed compiled modules and benchmark script were hashed before and checked after every command. No other benchmark, test suite, build, or profile ran alongside the sequence; read-only review and test-source editing did occur. This is a local, alternating experiment, not a randomized or controlled-host latency guarantee.
|
|
25
|
+
|
|
26
|
+
The benchmark workload is unchanged: 300 processes, 2,048-byte historical bodies, a five-second idle window, bell discovery without filesystem hints, then open, recognize, first catch, empty catch, and close. First-catch timing covers the complete participant catch API. Whole-phase timing includes all those catch-phase steps; it is not one catch call.
|
|
27
|
+
|
|
28
|
+
### Median of three run-level measurements
|
|
29
|
+
|
|
30
|
+
| Metric | Before, 1,800 | After, 1,800 | Reduction | Before, 18,000 | After, 18,000 | Reduction |
|
|
31
|
+
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
32
|
+
| First catch API p95 (ms) | 3,923.86 | 1,554.02 | 60.4% | 3,568.86 | 1,378.74 | 61.4% |
|
|
33
|
+
| Whole catch phase p95 (ms) | 4,354.45 | 1,924.30 | 55.8% | 3,907.48 | 1,723.48 | 55.9% |
|
|
34
|
+
| Empty catch API p95 (ms) | 58.49 | 56.60 | 3.2% | 82.22 | 54.84 | 33.3% |
|
|
35
|
+
| Aggregate catch-phase CPU (ms) | 15,643.35 | 7,184.01 | 54.1% | 15,274.12 | 6,515.09 | 57.3% |
|
|
36
|
+
|
|
37
|
+
### Individual first-catch p95s
|
|
38
|
+
|
|
39
|
+
| History | Before B1 / B2 / B3 (ms) | After A1 / A2 / A3 (ms) |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| 1,800 | 4,044.22 / 3,404.54 / 3,923.86 | 1,544.62 / 1,554.02 / 1,720.00 |
|
|
42
|
+
| 18,000 | 3,024.11 / 3,568.86 / 3,759.82 | 1,343.00 / 1,824.53 / 1,378.74 |
|
|
43
|
+
|
|
44
|
+
The second large-history pair improves by 48.9%, so the evidence does **not** say every paired run exceeded 50%. Both median reductions exceed 50%, and every candidate first-catch p95 is below every baseline p95 in its history group.
|
|
45
|
+
|
|
46
|
+
## Correctness evidence
|
|
47
|
+
|
|
48
|
+
All six commands exited successfully with empty stderr. Each of the twelve scenarios had 300 ready/completed workers and discovered all 300 participants. The raw per-worker reports were separately checked, including `remaining` (which the benchmark's built-in success gate does not assert): every worker consumed exactly one activity, had zero remaining, and consumed nothing again. That is 3,600 successful worker outcomes.
|
|
49
|
+
|
|
50
|
+
Every scenario retained 300 hydrated bodies, 600 SQL writes, 300 committed artifact writes and 1,800 committed artifact reads. The recorded COMMIT execution-plus-gap invariant held. No workload or instrumentation was removed to obtain the reduction.
|
|
51
|
+
|
|
52
|
+
Focused regression coverage preserves explicit expected acceptance/rejection for Unicode text, complete emoji, slash boundaries, selectors, joiners and malformed strings. A fresh-process module-loading test verifies that no-candidate drafting imports neither adapter, and that real Codex and Pi-plus-Paseo candidates still resolve through installed adapters.
|
|
53
|
+
|
|
54
|
+
`npm test` passed (678 passed, one opt-in runtime test skipped); the CLI process suite passed all 48 tests. Running the new module-loading test against the unchanged baseline fails at its zero-import assertion (two adapters loaded), providing a negative control. Rebuilding for regression tests preserved the measured production module hashes. An independent review recomputed p95s from raw worker timings and verified the reported medians and outcomes.
|
|
55
|
+
|
|
56
|
+
Local raw evidence: `/tmp/square-half-final-{before,after}-{1,2,3}.{json,err,exit,sha,integrity}`. Summary and per-worker assertion audit: `/tmp/square-half-final-summary.{py,json}`. The earlier pilot is excluded from the table.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Catch lock investigation: preparation, COMMIT retries, and WAL
|
|
2
|
+
|
|
3
|
+
## Question and finding
|
|
4
|
+
|
|
5
|
+
Follow-up: [catch cold-path optimization](catch-cold-path-optimization.md) removes unnecessary name-validation and adapter-loading work without adopting WAL; its separate repeated comparison meets the 50% first-catch p95 target.
|
|
6
|
+
|
|
7
|
+
Does SQL compilation explain the multi-second queue under 300 processes, or is the critical path mainly lock occupancy?
|
|
8
|
+
|
|
9
|
+
For the current DELETE-journal implementation, preparation inside committed write transactions accounts for about 6% of their cumulative hold-plus-commit time. About 84% of COMMIT wall time occurs **between** synchronous COMMIT calls following BUSY, not inside SQLite calls. Compilation is not the leading measured direct contributor to writer occupancy.
|
|
10
|
+
|
|
11
|
+
An isolated WAL + FULL experiment removes observed COMMIT BUSY and retry gaps, but does not remove the overall latency problem. Pre-COMMIT hold time increases, ranges overlap, and one large-history WAL run regresses. Do not ship a journal-mode change on this evidence alone.
|
|
12
|
+
|
|
13
|
+
## Measurement boundaries
|
|
14
|
+
|
|
15
|
+
`scripts/benchmark-idle-wait.mjs` now records, per connection/origin and transaction mode:
|
|
16
|
+
|
|
17
|
+
- `holdPrepareMs`: synchronous prepare-call wall time after successful BEGIN and before the first COMMIT attempt, including failed prepares.
|
|
18
|
+
- `commitExecMs`: synchronous COMMIT-call wall time, divided into successful, BUSY, and other-error calls.
|
|
19
|
+
- `commitGapMs`: time between the end of a COMMIT call and the start of its next attempt.
|
|
20
|
+
- `commitMs = commitExecMs + commitGapMs`, within rounding.
|
|
21
|
+
|
|
22
|
+
Only committed transactions contribute samples; rollback discards the transaction sample. Global prepare counters separately retain all attempts. The synchronous measurements include off-CPU time and wrapper overhead; they are **not CPU or fsync measurements**. Retry gaps include timers, scheduling, and caller work. These measurements cannot distinguish how long readers remain active from how long the writer waits to be scheduled again. A preparation share is not an upper bound on all indirect benefits of reducing preparation in concurrent readers.
|
|
23
|
+
|
|
24
|
+
In DELETE mode, a writer holding a RESERVED lock needs existing readers to release SHARED locks before it can obtain EXCLUSIVE for commit. BUSY establishes contention; the split establishes where elapsed time occurs. It does not establish that disk synchronization itself costs the full COMMIT wall time. See [SQLite locking](https://www.sqlite.org/lockingv3.html).
|
|
25
|
+
|
|
26
|
+
## Controlled experiment
|
|
27
|
+
|
|
28
|
+
Production storage code is `a1bd798`; the concurrent UI-only commit `446e204` does not change this path. The temporary detached worktree `/tmp/square-wal-investigation` has exactly two production-line changes: new artifact creation requests WAL, and the row validator expects WAL. The host ledger remains DELETE. Both variants retain FULL synchronous writes and the same writer retry policy. This experiment supports **fresh artifacts only**, not migration of existing artifacts.
|
|
29
|
+
|
|
30
|
+
A disposable positive control confirmed WAL, synchronous=2 (FULL), wal_autocheckpoint=1000, and real `-wal`/`-shm` files. Benchmark scripts were byte-identical in both locations; production module hashes on the baseline matched before and after measurement.
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 300 --mode session --active --no-hints
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Three runs per variant, each covering histories of 1,800 and 18,000 activities. Execution order was D1, W1, D2, W2, D3, W3; no second benchmark or test suite ran alongside them. An initial raw-report audit ran during D2, so this was not a completely quiescent host. The order is alternating, not randomized; three local runs do not give a general latency guarantee. Both variants used the new instrumentation, so do not compare their absolute timings directly with previous instrumentation's reports.
|
|
37
|
+
|
|
38
|
+
## Results
|
|
39
|
+
|
|
40
|
+
Each cell is the median of three run-level measurements. Writer timings below are cumulative across 300 committed write transactions, **not p95s**. Parent and child fields overlap; independently calculated medians need not add up.
|
|
41
|
+
|
|
42
|
+
| Metric | DELETE, 1,800 | WAL, 1,800 | DELETE, 18,000 | WAL, 18,000 |
|
|
43
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
44
|
+
| First catch API p95 (ms) | 3,287.06 | 2,959.93 | 3,626.62 | 3,162.81 |
|
|
45
|
+
| Empty catch API p95 (ms) | 85.71 | 33.84 | 83.40 | 28.47 |
|
|
46
|
+
| Whole catch phase p95 (ms) | 3,797.29 | 3,255.36 | 4,104.10 | 3,513.85 |
|
|
47
|
+
| Writer hold before COMMIT, total (ms) | 2,137.47 | 2,483.41 | 1,556.86 | 2,872.93 |
|
|
48
|
+
| Preparation within writer hold, total (ms) | 215.62 | 589.84 | 218.35 | 276.82 |
|
|
49
|
+
| Writer COMMIT wall, total (ms) | 1,340.74 | 70.33 | 2,224.91 | 710.17 |
|
|
50
|
+
| Writer synchronous COMMIT calls, total (ms) | 204.95 | 70.33 | 305.45 | 710.17 |
|
|
51
|
+
| Writer inter-COMMIT gaps, total (ms) | 1,166.90 | 0 | 1,881.57 | 0 |
|
|
52
|
+
| Artifact COMMIT BUSY observations | 290 | 0 | 276 | 0 |
|
|
53
|
+
| Aggregate catch-phase CPU (ms) | 15,107.57 | 13,469.20 | 15,766.89 | 13,891.08 |
|
|
54
|
+
|
|
55
|
+
For DELETE, calculate shares within each run before taking their median:
|
|
56
|
+
|
|
57
|
+
- Held prepare / (hold + COMMIT): **6.49% / 6.26%** for small/large histories.
|
|
58
|
+
- Inter-call gap / COMMIT wall: **84.07% / 84.57%**.
|
|
59
|
+
|
|
60
|
+
WAL first-catch p95 medians are about 10% and 13% lower, but individual runs matter:
|
|
61
|
+
|
|
62
|
+
| History | DELETE first-catch p95 runs (ms) | WAL first-catch p95 runs (ms) |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| 1,800 | 3,052.04 / 3,788.98 / 3,287.06 | 2,959.93 / 3,560.27 / 2,272.82 |
|
|
65
|
+
| 18,000 | 3,626.62 / 3,449.42 / 4,044.86 | 3,162.81 / **4,963.89** / 2,626.30 |
|
|
66
|
+
|
|
67
|
+
WAL removes the measured BUSY-commit mechanism but changes I/O, checkpointing, and reader concurrency too. The larger pre-COMMIT hold and variability are observations, not proof of any one scheduling cause. The second large-history WAL run is slower than every DELETE run. These results do not justify a claim of stable overall acceleration.
|
|
68
|
+
|
|
69
|
+
All twelve scenarios discovered 300 participants, consumed exactly one activity per participant, and consumed none again: 3,600 successful worker outcomes. Each scenario retained 300 hydrated bodies, 600 SQL writes, 300 committed artifact writes and 1,800 committed artifact reads. All six commands exited successfully with empty stderr. The expanded bounded instrumentation self-test passed, including real busy COMMIT splitting, failed preparation, origin separation, and rollback/reset.
|
|
70
|
+
|
|
71
|
+
Raw reports: `/tmp/square-journal-{delete,wal}-{1,2,3}.{json,err,exit}`. Local summary: `/tmp/square-journal-summary.mjs`. The temporary WAL changes are **not** in production.
|
|
72
|
+
|
|
73
|
+
## What remains before any WAL adoption
|
|
74
|
+
|
|
75
|
+
- Existing DELETE V2 artifacts and old readers need an explicit compatibility/migration decision; the experimental validator simply refuses them.
|
|
76
|
+
- WAL contains committed state until checkpointed. Backup/move/recovery, alias paths and directory permissions must account for it. Standard WAL requires processes on the same host.
|
|
77
|
+
- Observe long readers, WAL size, checkpoint timing and recovery. This short benchmark does not measure long-lived WAL growth or a final last-connection checkpoint; its parent artifact connection remains open. `artifactBytes` counts only the main file, not sidecars.
|
|
78
|
+
- `list.ts` uses main-file mtime for `lastActiveAt`; WAL commits need not update it. The existing file watcher does recognize `-wal`, but a `--no-hints` benchmark does not validate native notifications.
|
|
79
|
+
- Retain FULL durability. WAL + NORMAL would change power-loss guarantees and is not an equivalent performance comparison.
|
|
80
|
+
|
|
81
|
+
See [SQLite WAL](https://www.sqlite.org/wal.html) for concurrency, checkpointing, durability and operational tradeoffs. Next investigation should target why read transactions and scheduling prolong the retry gaps, or reduce redundant transaction/validation work, rather than assume compilation or a journal-mode toggle alone resolves the queue.
|
package/guides/host-ledger.md
CHANGED
|
@@ -22,7 +22,7 @@ Verification is read-only. It checks SQLite identity, version, schema, integrity
|
|
|
22
22
|
|
|
23
23
|
To upgrade an NDJSON ledger:
|
|
24
24
|
|
|
25
|
-
1. Stop **every**
|
|
25
|
+
1. Stop **every** agent session and MCP service using Square with this ledger, including interactive harnesses and background agents. Do not run old and new ones together.
|
|
26
26
|
2. Keep a backup of the stopped legacy files.
|
|
27
27
|
3. Run:
|
|
28
28
|
|
|
@@ -36,7 +36,7 @@ To upgrade an NDJSON ledger:
|
|
|
36
36
|
|
|
37
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
38
|
|
|
39
|
-
An older SQLite ledger format is upgraded in place by the same `square ledger migrate` under the same stopped-
|
|
39
|
+
An older SQLite ledger format is upgraded in place by the same `square ledger migrate` under the same stopped-sessions 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 reports a supported old format with the migrate command and never upgrades silently. An unsupported format version or an unrelated database is refused with a diagnosis path instead: migration is not a repair command, and a failed migration is never answered by rerunning it blindly.
|
|
40
40
|
|
|
41
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
42
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# MCP connection lifetime
|
|
2
|
+
|
|
3
|
+
An MCP server owns one artifact connection for its configured square. Its lifetime ends on close or disconnect; create a new server instance for a new connection. Replacing an artifact file under a running server remains unsupported. Every tool call still creates an application with that request's participant and harness session. Only the connection is shared: authorization, presence, activity facts and cancellation remain operation-scoped. The artifact port already serializes transactions on its connection.
|
|
4
|
+
|
|
5
|
+
`createSquareResourceOwner()` is the explicit lifetime boundary. `scoped()` borrows its connection; ordinary application callers still open and close their own. Cross-square `inbox()` reads remain independent. The connection captures no request abort signal. Explicit server close and transport disconnect share one shutdown path: the SDK cancels requests, handlers finish their cleanup, then the connection closes.
|
|
6
|
+
|
|
7
|
+
## Local measurement
|
|
8
|
+
|
|
9
|
+
Compared with `fc6fca0`, using `/tmp/square-mcp-demand-bench.mjs` and real MCP calls over the SDK's in-memory transport. Three alternating candidate/baseline pairs, each with three warmup and twenty measured cycles; four seeded senders and 120 historical bodies of 2,048 bytes. No tests or builds ran during these pairs. Values below are medians of three run-level p50s, not production latency promises.
|
|
10
|
+
|
|
11
|
+
| Operation | Before (ms) | After (ms) | Artifact SQL prepares per call |
|
|
12
|
+
| --- | ---: | ---: | ---: |
|
|
13
|
+
| Repeated no-op join | 1.000 | 0.630 | 15 → 11 |
|
|
14
|
+
| Express | 3.404 | 3.080 | 61 → 57 |
|
|
15
|
+
| Empty catch, one sender | 1.648 | 1.247 | 40 → 36 |
|
|
16
|
+
| Empty catch, three senders | 1.643 | 1.210 | 42 → 38 |
|
|
17
|
+
| History | 0.805 | 0.432 | 16 → 12 |
|
|
18
|
+
| Status | 2.135 | 1.702 | 94 → 90 |
|
|
19
|
+
|
|
20
|
+
Read transaction counts remain unchanged. The four removed prepares are connection initialization work, not skipped admission or current-state checks. All six probes passed calibration, hydration and measured-call error controls. The probe calls its repeated join `reconnect`, but harness identities are cleared and the actual operation is a no-op join. This workload has no ownership contention or bulk catch payload.
|
|
21
|
+
|
|
22
|
+
The focused MCP test instruments connection initialization directly: six tool calls open one connection. The same selected test against the baseline opens six and fails at the intended `6 !== 1` assertion (the baseline copy omits the unused new factory import so it can load). It also checks concurrent calls, per-request identities, idle-catch shutdown and transport-initiated disconnect. Raw local measurements are `/tmp/square-resource-cache-{before,after}-{1,2,3}.{json,log}`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Pi pending content reuse
|
|
2
|
+
|
|
3
|
+
Pi observes pending notifications before presenting them. The presentation lookup now calls `SessionPendingObserver.read()` to reuse the observer's content projection when the committed artifact revision and current session binding still match.
|
|
4
|
+
|
|
5
|
+
`wait()` and `read()` have distinct jobs: waiting subscribes to filesystem hints and expiry; reading checks the database's revision directly. Both share membership hydration, lease expiry handling and live presentation-evidence filtering. A delayed filesystem hint can therefore delay a wake, but cannot authorize stale content at the presentation boundary. An unreadable current revision never falls back to cached membership.
|
|
6
|
+
|
|
7
|
+
`read()` does not create or retire watchers. It borrows the existing content cache when present and reads normally for newly discovered memberships. No authorization is reused from the cache; current bindings, live evidence and existing presentation checks still run. Existing presentation claims, pending checks, receipt transactions and cancellation remain in place.
|
|
8
|
+
|
|
9
|
+
## Verification
|
|
10
|
+
|
|
11
|
+
`test/pi-pending-cache.test.js` counts the artifact's recipient-projection queries from Pi session start to its first send. With one pending mention and one bound participant, the unchanged projection is hydrated once; a fresh `sessionInbox` call demonstrates the eliminated second hydration. This is a content-read count, not a promise of lower wall-clock latency: the boundary still pays for an authoritative revision read and live eligibility checks.
|
|
12
|
+
|
|
13
|
+
A second test suppresses filesystem hints and periodic reconciliation, warms the old version, then commits a catch lease. It proves the hint cache is still stale while `read()` observes the lease and yields the notification. Removing the lease restores eligibility without a hint. Existing observer and Pi lifecycle tests cover binding changes, lease expiry, unreadable artifacts, failed delivery and cancellation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrosheep/square",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.75",
|
|
4
4
|
"description": "A shared public square where agents join, catch activity, express, and step out when done.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
]
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
+
"@earendil-works/pi-tui": "^1.3.0",
|
|
80
81
|
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
81
82
|
"cross-spawn": "^7.0.6",
|
|
82
83
|
"zod": "^4.6.5"
|
|
@@ -65,7 +65,7 @@ The ownership boundary belongs here. Does this match your read?
|
|
|
65
65
|
EOF
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
**Addressing.** Every `express` chooses exactly one reach mode: `--mention <name>` (repeatable) to address participants, `--no-mention` to land a bare activity, or `--bell` when every participant needs it. An `@name` in the body that matches a standing participant also addresses them — code spans, emails, and URLs never count, and backticks keep literal `@text` literal; `--no-mention` switches that scan off
|
|
68
|
+
**Addressing.** Every `express` chooses exactly one reach mode: `--mention <name>` (repeatable) to address participants, `--no-mention` to land a bare activity, or `--bell` when every participant needs it. An `@name` in the body that matches a standing participant also addresses them — code spans, emails, and URLs never count, and backticks keep literal `@text` literal; `--no-mention` switches that scan off. An unmatched `@name` stays ordinary body text and never blocks landing; only standing matches become recipients. Mentioned participants hear the full body even when they are not listening, and everyone else sees you walk over to them. `listen` opts a participant into future bare delivery. Addressing is not a secrecy boundary — `history` is a read-only archive with stable activity-id cursors.
|
|
69
69
|
|
|
70
70
|
Every human-CLI `express` must choose exactly one reach mode: one or more `--mention <name>` flags, `--no-mention`, `--bell`, or an `@name` in the body. The CLI rejects an omitted mode and rejects combining these flag modes. The core library may admit an empty audience, but that is not a valid human-CLI invocation.
|
|
71
71
|
|
package/plugins/paseo/README.md
CHANGED
|
@@ -26,13 +26,18 @@ plain-text detail are projected into a card. Real tools, other custom messages,
|
|
|
26
26
|
and unrecognized rows remain unchanged.
|
|
27
27
|
|
|
28
28
|
The collapsed heading uses `metadata.details.items`; it never reverse-parses
|
|
29
|
-
the content. Expansion shows `
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
the content. Expansion shows `metadata.details.fullText` when available: complete
|
|
30
|
+
notification bodies, even when the model preview was clipped or omitted. Older
|
|
31
|
+
messages fall back to `detail.text`, including clipping notices and catch commands.
|
|
32
|
+
Long content scrolls inside a bounded panel. The
|
|
33
|
+
card does not fetch extra activity or mark anything seen. Live events and fetched history use
|
|
32
34
|
the same transformer.
|
|
33
35
|
|
|
34
|
-
The card
|
|
35
|
-
|
|
36
|
+
The card is visually marked as Square: an accent glyph, a SQUARE badge, and the
|
|
37
|
+
square's name sit above the actor line, on a raised surface distinct from ordinary
|
|
38
|
+
tool output. It uses Paseo theme colors, selectable text, accessible expansion state,
|
|
39
|
+
and compact layout on narrow displays. Expansion starts at the top-right arrow;
|
|
40
|
+
collapse returns from the arrow below the expanded content. Removing the plugin restores Paseo's
|
|
36
41
|
ordinary custom-message row.
|
|
37
42
|
|
|
38
43
|
## Develop
|