@astrosheep/square 0.3.88 → 0.3.89
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.js +11 -9
- package/dist/boundary-presentation.d.ts +3 -2
- package/dist/boundary-presentation.js +50 -77
- package/dist/cli/context.js +1 -3
- package/dist/delivery-deferral.d.ts +28 -0
- package/dist/delivery-deferral.js +83 -0
- package/dist/delivery-operations.js +8 -6
- package/dist/delivery.js +4 -1
- package/dist/inbox.js +21 -10
- package/dist/mcp.js +0 -3
- package/dist/model.d.ts +1 -0
- package/dist/operation-presentation.js +0 -4
- package/dist/ports.d.ts +11 -3
- package/dist/presentation-operations.d.ts +20 -1
- package/dist/presentation-operations.js +68 -44
- package/dist/sqlite-square.js +35 -13
- package/dist/square-domain.d.ts +1 -1
- package/dist/square-domain.js +12 -0
- package/dist/square-facade.d.ts +8 -2
- package/dist/square-migration.d.ts +2 -1
- package/dist/square-migration.js +25 -26
- package/dist/square-sqlite.d.ts +5 -2
- package/dist/square-sqlite.js +79 -6
- package/dist/square-stdio.d.ts +1 -1
- package/dist/square-stdio.js +30 -17
- package/dist/square-tools.d.ts +1 -1
- package/dist/square-tools.js +25 -51
- package/dist/stdio-server.js +23 -3
- package/dist/views.js +0 -2
- package/guides/backends.md +26 -13
- package/guides/change-waits.md +40 -11
- package/guides/claude-native.md +9 -9
- package/guides/mcp.md +1 -1
- package/guides/migration.md +30 -12
- package/guides/operation-cancellation.md +16 -1
- package/guides/participant-ownership.md +60 -186
- package/guides/pi-tools.md +1 -1
- package/guides/square-api.md +72 -9
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +3 -3
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/README.md +7 -6
- package/plugins/paseo/client/markdown.tsx +125 -185
- package/plugins/paseo/client/square-message.tsx +9 -25
- package/plugins/paseo/client/transform.ts +5 -4
- package/plugins/paseo/index.client.tsx +14 -12
- package/plugins/paseo/package.json +3 -0
- package/plugins/paseo/shared/message.ts +50 -1
- package/plugins/pi/square-pi.js +1 -1
- package/skills/square/SKILL.md +3 -3
package/dist/square-stdio.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { validateMetadataMatch, validateDeferralDuration, validateDeferralId } from './delivery-deferral.js';
|
|
1
3
|
import { spawn } from 'node:child_process';
|
|
2
4
|
import path from 'node:path';
|
|
3
5
|
import { createInterface } from 'node:readline';
|
|
4
6
|
import { SquareError } from './model.js';
|
|
5
7
|
import { harnessSessionSources } from './participant-identity.js';
|
|
6
8
|
import { requireJoinName } from './seats.js';
|
|
7
|
-
export const STDIO_PROTOCOL_VERSION =
|
|
9
|
+
export const STDIO_PROTOCOL_VERSION = 2;
|
|
8
10
|
export const STDIO_MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
9
11
|
export const identityVariables = [...harnessSessionSources.flatMap(source => [source.variable, ...(source.childVariable ? [source.childVariable] : [])])];
|
|
10
12
|
export function wireIdentity(env) {
|
|
@@ -157,33 +159,44 @@ class StdioSquare {
|
|
|
157
159
|
this.connection = connection;
|
|
158
160
|
this.handle = handle;
|
|
159
161
|
}
|
|
160
|
-
call(method, args, control, participant) {
|
|
161
|
-
return this.connection.request(method, { handle: this.handle, args, ...(participant === undefined ? {} : { participant }) }, control);
|
|
162
|
+
call(method, args, control, participant, participantHandle) {
|
|
163
|
+
return this.connection.request(method, { handle: this.handle, args, ...(participant === undefined ? {} : { participant, participantHandle }) }, control);
|
|
162
164
|
}
|
|
163
|
-
participantHandle(name) {
|
|
165
|
+
participantHandle(name, reference) {
|
|
166
|
+
const call = (method, args, control) => this.call(method, args, control, name, reference);
|
|
164
167
|
return {
|
|
165
168
|
name,
|
|
166
169
|
express: ((bodyOrInput, optionsOrControl, control) => {
|
|
167
170
|
if (typeof bodyOrInput === 'object' && bodyOrInput !== null)
|
|
168
|
-
return
|
|
169
|
-
return
|
|
171
|
+
return call('express', [bodyOrInput, undefined], optionsOrControl);
|
|
172
|
+
return call('express', [bodyOrInput, optionsOrControl], control);
|
|
170
173
|
}),
|
|
171
|
-
catch: (options, control) =>
|
|
172
|
-
listen: (target, options, control) =>
|
|
173
|
-
ignore: (target, options, control) =>
|
|
174
|
-
unlisten: (target, options, control) =>
|
|
175
|
-
listening: (options, control) =>
|
|
176
|
-
hold: (reason, options, control) =>
|
|
177
|
-
resume: (options, control) =>
|
|
178
|
-
done: (body, options, control) =>
|
|
174
|
+
catch: (options, control) => call('catch', [options], control),
|
|
175
|
+
listen: (target, options, control) => call('listen', [target, options], control),
|
|
176
|
+
ignore: (target, options, control) => call('ignore', [target, options], control),
|
|
177
|
+
unlisten: (target, options, control) => call('unlisten', [target, options], control),
|
|
178
|
+
listening: (options, control) => call('listening', [options], control),
|
|
179
|
+
hold: (reason, options, control) => call('hold', [reason, options], control),
|
|
180
|
+
resume: (options, control) => call('resume', [options], control),
|
|
181
|
+
done: (body, options, control) => call('done', [body, options], control),
|
|
182
|
+
deferDelivery: async (input, control) => {
|
|
183
|
+
const match = validateMetadataMatch(input?.match), durationMs = validateDeferralDuration(input?.durationMs);
|
|
184
|
+
return call('deferDelivery', [{ match, durationMs }], control);
|
|
185
|
+
},
|
|
186
|
+
renewDeliveryDeferral: async (id, durationMs, control) => call('renewDeliveryDeferral', [validateDeferralId(id), validateDeferralDuration(durationMs)], control),
|
|
187
|
+
releaseDeliveryDeferral: async (id, control) => call('releaseDeliveryDeferral', [validateDeferralId(id)], control),
|
|
179
188
|
history: (query, control) => this.history(query, control),
|
|
180
189
|
};
|
|
181
190
|
}
|
|
182
|
-
async participant(name) {
|
|
191
|
+
async participant(name) {
|
|
192
|
+
const reference = randomUUID();
|
|
193
|
+
return this.participantHandle(await this.call('participant', [name, reference]), reference);
|
|
194
|
+
}
|
|
183
195
|
async join(...[name, options, control]) {
|
|
184
196
|
requireJoinName(name);
|
|
185
|
-
const
|
|
186
|
-
|
|
197
|
+
const reference = randomUUID();
|
|
198
|
+
const result = await this.call('join', [name, options, reference], control);
|
|
199
|
+
return { ...result, participant: this.participantHandle(result.participant, reference) };
|
|
187
200
|
}
|
|
188
201
|
history(...[query, control]) { return this.call('history', [query], control); }
|
|
189
202
|
status(...[options, control]) { return this.call('status', [options], control); }
|
package/dist/square-tools.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ interface ToolDefinition {
|
|
|
16
16
|
export interface ToolControl extends OperationControl {
|
|
17
17
|
readonly observe?: boolean;
|
|
18
18
|
}
|
|
19
|
-
export declare const SQUARE_TOOL_INSTRUCTIONS = "
|
|
19
|
+
export declare const SQUARE_TOOL_INSTRUCTIONS = "The catch tool consumes activities. Use history with act/<index> cursors for read-only archive access.";
|
|
20
20
|
export declare const SQUARE_TOOLS: readonly ToolDefinition[];
|
|
21
21
|
export declare function squareToolFailure(error: unknown, signal?: AbortSignal): JsonObject;
|
|
22
22
|
/** Readable failure text with the same recovery commands the CLI offers, never raw JSON. */
|
package/dist/square-tools.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod/v4';
|
|
2
2
|
import { isSquareError } from './model.js';
|
|
3
3
|
import { CATCH_DEFAULT_LIMIT } from './square-api.js';
|
|
4
|
-
import { renderCatchOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderStatusResult, } from './operation-presentation.js';
|
|
5
|
-
const location = z.string().optional().describe('
|
|
6
|
-
const participant = z.string().optional().describe('
|
|
4
|
+
import { renderCatchOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderListeningResult, renderParticipantsResult, renderStatusResult, } from './operation-presentation.js';
|
|
5
|
+
const location = z.string().optional().describe('Local .square artifact path or configured backend URI (stdio://name/key). Relative paths use the current working directory. Defaults to the configured square.');
|
|
6
|
+
const participant = z.string().optional().describe('Participant name. Defaults to the configured participant or the current session’s unambiguous participant.');
|
|
7
7
|
const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
|
|
8
8
|
const stringArray = z.array(z.string());
|
|
9
9
|
/** Keep the observation request internal: it is not an argument a caller decides. */
|
|
@@ -48,69 +48,43 @@ function tool(name, description, shape, run, render) {
|
|
|
48
48
|
return filtered;
|
|
49
49
|
} };
|
|
50
50
|
}
|
|
51
|
-
export const SQUARE_TOOL_INSTRUCTIONS = '
|
|
52
|
-
const target = z.string().describe('The participant to turn toward or away from.');
|
|
51
|
+
export const SQUARE_TOOL_INSTRUCTIONS = 'The catch tool consumes activities. Use history with act/<index> cursors for read-only archive access.';
|
|
53
52
|
export const SQUARE_TOOLS = [
|
|
54
|
-
tool('join', '
|
|
55
|
-
as: z.string().optional().describe('Your name in the square. Give it explicitly when joining.'), takeover: z.boolean().optional().describe('Claim this name even though another session holds it now. That session loses it.'),
|
|
56
|
-
}, async (square, { as, takeover }, control) => {
|
|
53
|
+
tool('join', 'Join the square as a participant.', { as: participant, takeover: z.boolean().optional().describe('Take the name from its current session owner.') }, async (square, { as, takeover }, control) => {
|
|
57
54
|
const { participant, ...receipt } = await square.join(as, { takeover, ...observation(control) }, control);
|
|
58
55
|
return { ...receipt, participant: participant.name };
|
|
59
56
|
}, rendering((result) => renderJoinResult(result))),
|
|
60
|
-
tool('express', '
|
|
61
|
-
as: participant,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
reply: activityId.optional().describe('The activity you are answering, like act/12. Its author hears you if still here.'),
|
|
66
|
-
noMention: z.boolean().optional().describe('Leave @names in the body as plain text. Mentions and reply still reach people.'),
|
|
67
|
-
force: z.boolean().optional().describe('Speak now, even if something reached you that you have not caught yet.'),
|
|
68
|
-
noWait: z.boolean().optional().describe('If a hand is raised or you are speaking too fast, return at once instead of waiting.'),
|
|
57
|
+
tool('express', 'Express a message or embodied action in the square. Mentions and replies direct attention; bell reaches everyone and keeps body @names literal. Bell cannot combine with explicit mentions. Bare activity is valid even with no listeners.', {
|
|
58
|
+
as: participant, body: z.string(), mentions: stringArray.optional(), reach: z.enum(['bell']).optional(),
|
|
59
|
+
reply: activityId.optional(), noMention: z.boolean().optional().describe('Disable body @name scanning; explicit mentions and reply still apply.'),
|
|
60
|
+
force: z.boolean().optional().describe('Speak without first catching unread activity or satisfying attention etiquette.'),
|
|
61
|
+
noWait: z.boolean().optional().describe('Return an error rather than waiting when held or throttled.'),
|
|
69
62
|
}, async (square, { body, as, ...options }, control) => {
|
|
70
63
|
const { delivery: _delivery, ...result } = await (await square.participant(as)).express(body, { ...options, ...observation(control) }, control);
|
|
71
64
|
return result;
|
|
72
65
|
}, rendering((result) => renderExpression(result.observation?.location ?? '', result))),
|
|
73
|
-
tool('catch', '
|
|
74
|
-
as: participant,
|
|
75
|
-
|
|
76
|
-
idle: z.number().finite().min(0).optional().describe('When nothing is waiting, wait until something arrives or until it has been quiet this many milliseconds. Leave it out to return at once.'),
|
|
77
|
-
from: stringArray.optional().describe('Only what these participants said.'),
|
|
78
|
-
mention: z.boolean().optional().describe('Only what was addressed to you, and bells.'),
|
|
79
|
-
limit: z.number().int().min(1).optional().describe('At most this many at once (default 10, up to 100). The rest keeps waiting.'),
|
|
66
|
+
tool('catch', 'Catch and consume activities perceptible to the participant. idle waits for relevant activity or this many milliseconds of quiet; omitted means immediate. id catches one activity and cannot be combined with filters, idle or limit.', {
|
|
67
|
+
as: participant, id: activityId.optional(), idle: z.number().finite().min(0).optional(), from: stringArray.optional(),
|
|
68
|
+
mention: z.boolean().optional(), limit: z.number().int().min(1).optional(),
|
|
80
69
|
}, async (square, { as, ...options }, control) => (await square.participant(as)).catch({ ...options, ...observation(control) }, control), rendering((result, args) => {
|
|
81
70
|
// The CLI always spells the default cap in its continuation, so the tool does too.
|
|
82
71
|
const options = args;
|
|
83
72
|
const renderOptions = options.id === undefined ? { ...options, limit: options.limit ?? CATCH_DEFAULT_LIMIT } : options;
|
|
84
73
|
return renderCatchOutcome(result.observation?.location ?? '', result, renderOptions).stdout;
|
|
85
74
|
})),
|
|
86
|
-
tool('history', '
|
|
87
|
-
limit: z.number().int().min(1).optional().
|
|
88
|
-
|
|
89
|
-
before: activityId.optional().describe('The page just before this activity, like act/12.'),
|
|
90
|
-
after: activityId.optional().describe('The page just after this activity, like act/12.'),
|
|
91
|
-
grep: z.string().optional().describe('Only activities matching this regular expression in their names, ids or words. Use grep or fixed, not both.'),
|
|
92
|
-
fixed: z.string().optional().describe('Only activities containing this exact text.'),
|
|
93
|
-
from: stringArray.optional().describe('Only what these participants said.'),
|
|
94
|
-
mention: z.string().optional().describe('Only what was addressed to this participant.'),
|
|
75
|
+
tool('history', 'Read the activity stream without consuming it, using stable activity-id cursors. Default page size is 10, maximum 100. grep is a regex; fixed is literal text; use only one.', {
|
|
76
|
+
limit: z.number().int().min(1).optional(), order: z.enum(['asc', 'desc']).optional(), before: activityId.optional(), after: activityId.optional(),
|
|
77
|
+
grep: z.string().optional(), fixed: z.string().optional(), from: stringArray.optional(), mention: z.string().optional(),
|
|
95
78
|
}, (square, args, control) => square.history({ ...args, ...observation(control) }, control), rendering((result, args) => renderHistoryResult(result.observation?.location ?? '', result, { query: args }))),
|
|
96
|
-
tool('
|
|
97
|
-
|
|
98
|
-
}, async (square, { as,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
},
|
|
102
|
-
tool('
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
release: z.boolean().optional().describe('true lowers the raised hand so the conversation goes on.'),
|
|
106
|
-
}, async (square, { as, reason, release }, control) => {
|
|
107
|
-
const self = await square.participant(as);
|
|
108
|
-
return release === true ? self.resume(observation(control), control) : self.hold(reason, observation(control), control);
|
|
109
|
-
}, rendering((result, args) => renderLifecycleResult(result.observation?.location ?? '', args.release === true ? 'resume' : 'hold', result))),
|
|
110
|
-
tool('done', 'Step out of the square. Everyone sees you leave, and you stop listening to anyone. Leaving carries no words, so say goodbye with express first. Stay instead if someone may still need you; you can join again later.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).done(undefined, observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'done', result))),
|
|
111
|
-
tool('status', 'Look around the square: who is here, whether a hand is raised, the latest activity, and who you listen to.', {
|
|
112
|
-
as: z.string().optional().describe('Whose listening to show. Leave it out if you hold exactly one name here.'),
|
|
113
|
-
}, (square, { as }, control) => square.status({ viewer: as, ...observation(control) }, control), rendering((result) => renderStatusResult(result.observation?.location ?? '', result))),
|
|
79
|
+
tool('listen', 'Listen for future activity from a participant, clearing any ignore of them.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).listen(target, observation(control), control), rendering((result) => renderListenerChange(result.observation?.location ?? '', 'listen', result))),
|
|
80
|
+
tool('ignore', 'Block a participant’s future mentions and bare activity. Listening again clears the ignore; already landed activity is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).ignore(target, observation(control), control), rendering((result) => renderListenerChange(result.observation?.location ?? '', 'ignore', result))),
|
|
81
|
+
tool('unlisten', 'Stop listening to a participant’s future activity; mentions and replies still reach you. An existing ignore is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).unlisten(target, observation(control), control), rendering((result) => renderListenerChange(result.observation?.location ?? '', 'unlisten', result))),
|
|
82
|
+
tool('listening', 'List participants this participant is listening to.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).listening(observation(control), control), rendering((result) => renderListeningResult(result.observation?.location ?? '', result))),
|
|
83
|
+
tool('hold', 'Raise a hand to hold the square.', { as: participant, reason: z.string().optional() }, async (square, { as, reason }, control) => (await square.participant(as)).hold(reason, observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'hold', result))),
|
|
84
|
+
tool('resume', 'Release the current hold.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).resume(observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'resume', result))),
|
|
85
|
+
tool('done', 'Leave the square without adding a message.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).done(undefined, observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'done', result))),
|
|
86
|
+
tool('status', 'Read the public status of the square.', {}, (square, _args, control) => square.status(observation(control), control), rendering((result) => renderStatusResult(result.observation?.location ?? '', result))),
|
|
87
|
+
tool('participants', 'List participants standing in or done with the square.', {}, (square, _args, control) => square.participants(observation(control), control), rendering((result) => renderParticipantsResult(result.observation?.location ?? '', result))),
|
|
114
88
|
];
|
|
115
89
|
export function squareToolFailure(error, signal) {
|
|
116
90
|
const normalized = signal?.aborted
|
package/dist/stdio-server.js
CHANGED
|
@@ -16,6 +16,13 @@ export async function runSquareStdioServer(root) {
|
|
|
16
16
|
let square;
|
|
17
17
|
let location;
|
|
18
18
|
let opening = false, closing = false;
|
|
19
|
+
const participants = new Map();
|
|
20
|
+
const captureParticipant = (reference, participant) => {
|
|
21
|
+
if (typeof reference !== 'string' || reference.length === 0 || participants.has(reference))
|
|
22
|
+
throw new SquareError('invalid_args', 'Participant handle must be a fresh connection-local reference');
|
|
23
|
+
participants.set(reference, participant);
|
|
24
|
+
return participant.name;
|
|
25
|
+
};
|
|
19
26
|
const running = new Map();
|
|
20
27
|
const write = (message) => { if (!process.stdout.destroyed)
|
|
21
28
|
process.stdout.write(JSON.stringify(message) + '\n'); };
|
|
@@ -91,10 +98,12 @@ export async function runSquareStdioServer(root) {
|
|
|
91
98
|
const args = params.args.map((arg) => arg === null ? undefined : arg);
|
|
92
99
|
let result;
|
|
93
100
|
switch (method) {
|
|
94
|
-
case 'participant': return (await square.participant(args[0]))
|
|
101
|
+
case 'participant': return captureParticipant(args[1], await square.participant(args[0]));
|
|
95
102
|
case 'join': {
|
|
103
|
+
if (typeof args[2] !== 'string' || args[2].length === 0 || participants.has(args[2]))
|
|
104
|
+
throw new SquareError('invalid_args', 'Participant handle must be a fresh connection-local reference');
|
|
96
105
|
const joined = await square.join(args[0], args[1], control);
|
|
97
|
-
result = { ...joined, participant: joined.participant
|
|
106
|
+
result = { ...joined, participant: captureParticipant(args[2], joined.participant) };
|
|
98
107
|
break;
|
|
99
108
|
}
|
|
100
109
|
case 'history':
|
|
@@ -112,8 +121,19 @@ export async function runSquareStdioServer(root) {
|
|
|
112
121
|
default: {
|
|
113
122
|
if (typeof params.participant !== 'string')
|
|
114
123
|
throw new SquareError('invalid_args', 'Participant operation requires a name');
|
|
115
|
-
const participant =
|
|
124
|
+
const participant = participants.get(params.participantHandle);
|
|
125
|
+
if (participant === undefined || participant.name !== params.participant)
|
|
126
|
+
throw new SquareError('invalid_args', 'Select a captured participant handle on this connection');
|
|
116
127
|
switch (method) {
|
|
128
|
+
case 'deferDelivery':
|
|
129
|
+
result = await participant.deferDelivery(args[0], control);
|
|
130
|
+
break;
|
|
131
|
+
case 'renewDeliveryDeferral':
|
|
132
|
+
result = await participant.renewDeliveryDeferral(args[0], args[1], control);
|
|
133
|
+
break;
|
|
134
|
+
case 'releaseDeliveryDeferral':
|
|
135
|
+
result = await participant.releaseDeliveryDeferral(args[0], control);
|
|
136
|
+
break;
|
|
117
137
|
case 'express':
|
|
118
138
|
result = await participant.express(args[0], args[1], control);
|
|
119
139
|
break;
|
package/dist/views.js
CHANGED
|
@@ -158,7 +158,6 @@ export function listPresentation(facts) {
|
|
|
158
158
|
export function queryStatus(facts, context, viewer = '', observe = false) {
|
|
159
159
|
const participants = coreParticipants(facts, context.now);
|
|
160
160
|
const header = facts.header(), hold = facts.hold(), standingCount = inSquareCount(facts);
|
|
161
|
-
const listener = participants.find((p) => p.state !== 'not joined' && nameKey(p.name) === nameKey(viewer));
|
|
162
161
|
const latest = facts.activityHeads({ kinds: ['say', 'done'] }, { edge: 'newest', limit: 1 }).items[0];
|
|
163
162
|
const latestAct = latest === undefined ? undefined : facts.activity(latest.index);
|
|
164
163
|
const data = {
|
|
@@ -170,7 +169,6 @@ export function queryStatus(facts, context, viewer = '', observe = false) {
|
|
|
170
169
|
...(hold.reason === undefined ? {} : { reason: hold.reason }),
|
|
171
170
|
...(hold.at === undefined ? {} : { at: hold.at }),
|
|
172
171
|
} : null,
|
|
173
|
-
...(listener === undefined ? {} : { listening: listener.listening }),
|
|
174
172
|
participants: participants.filter((p) => p.state !== 'not joined'), now: context.now,
|
|
175
173
|
...(latestAct === undefined || latestAct.actor === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
|
|
176
174
|
};
|
package/guides/backends.md
CHANGED
|
@@ -55,9 +55,10 @@ import { Square, type SquareBackends } from '@astrosheep/square';
|
|
|
55
55
|
const backends: SquareBackends = {
|
|
56
56
|
local: { command: process.execPath, args: ['/absolute/path/to/square/dist/stdio-server.js', '--root', '/tmp/squares'] },
|
|
57
57
|
};
|
|
58
|
-
const square = await Square.at({ path: 'stdio://local/design', backends, participant: 'Bob'
|
|
58
|
+
const square = await Square.at({ path: 'stdio://local/design', backends, participant: 'Bob',
|
|
59
|
+
body: { channel: 'example', session: 'bob-process' } });
|
|
59
60
|
try {
|
|
60
|
-
const bob = (await square.join()).participant;
|
|
61
|
+
const bob = (await square.join('Bob')).participant;
|
|
61
62
|
const caught = await bob.catch();
|
|
62
63
|
console.log(caught.activities);
|
|
63
64
|
} finally {
|
|
@@ -99,7 +100,7 @@ configured local SQLite connection. Pi tools accept the identical location and r
|
|
|
99
100
|
- CLI failed-expression drafts stay in local `./drafts` for backend locations; recovery commands and
|
|
100
101
|
display labels retain the URI. Local-artifact draft placement is unchanged.
|
|
101
102
|
|
|
102
|
-
## Protocol version
|
|
103
|
+
## Protocol version 2
|
|
103
104
|
|
|
104
105
|
Newline-delimited JSON-RPC 2.0 over UTF-8 stdin/stdout, one object per line, at most 8 MiB per frame.
|
|
105
106
|
Stdout is protocol only. No batch requests. The initial versioned handshake opens one logical square
|
|
@@ -107,9 +108,9 @@ per connection and returns an opaque handle. IDs are unique positive integers on
|
|
|
107
108
|
requests may overlap and responses may arrive out of order.
|
|
108
109
|
|
|
109
110
|
```json
|
|
110
|
-
{"jsonrpc":"2.0","id":1,"method":"square/open","params":{"version":
|
|
111
|
-
{"jsonrpc":"2.0","id":1,"result":{"version":
|
|
112
|
-
{"jsonrpc":"2.0","id":2,"method":"join","params":{"handle":"square","args":["Bob",{"observe":true}]}}
|
|
111
|
+
{"jsonrpc":"2.0","id":1,"method":"square/open","params":{"version":2,"key":"design","location":"stdio://local/design","identity":{"PI_SESSION_ID":"session-id"},"body":{"channel":"pi","session":"session-id"},"participant":"Bob"}}
|
|
112
|
+
{"jsonrpc":"2.0","id":1,"result":{"version":2,"handle":"square"}}
|
|
113
|
+
{"jsonrpc":"2.0","id":2,"method":"join","params":{"handle":"square","args":["Bob",{"observe":true},"fresh-participant-reference"]}}
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
Open may include `build: {markdown, hardCap?, throttlePerMinute?, template?, force?}` instead of
|
|
@@ -120,25 +121,36 @@ coordinates (including PASEO_AGENT_ID), then overlays explicit call-local facts
|
|
|
120
121
|
`process.env`. Participant defaults remain separate from square keys. Each new tool open therefore
|
|
121
122
|
uses that call's identity, not MCP/Pi/backend startup identity.
|
|
122
123
|
|
|
123
|
-
After open, methods take `{handle, args
|
|
124
|
-
|
|
124
|
+
After open, square methods take `{handle, args}` and Participant methods additionally require
|
|
125
|
+
`{participant, participantHandle}`. Selection/join supply a fresh opaque connection-local reference;
|
|
126
|
+
the server retains the actual captured Participant under it instead of reselecting by name per
|
|
127
|
+
request. Version 2 deliberately refuses older peers rather than accepting unfenced handles.
|
|
128
|
+
`args` is the public operation's positional arguments excluding `OperationControl`, plus the
|
|
129
|
+
selection reference noted below; trailing JSON nulls represent omitted optional arguments.
|
|
125
130
|
Nested nulls such as `history({limit:null})` retain their public meaning.
|
|
126
131
|
|
|
127
132
|
| Methods | args | participant field |
|
|
128
133
|
| --- | --- | --- |
|
|
129
|
-
| `participant` | `[name
|
|
130
|
-
| `join` | `[name?,
|
|
134
|
+
| `participant` | `[name?, freshReference]` | no; selection only, not joining |
|
|
135
|
+
| `join` | `[name, options?, freshReference]` | no |
|
|
131
136
|
| `history`, `status`, `participants` | `[options?]` | no |
|
|
132
137
|
| `recovery` | `[name?]` | no |
|
|
133
138
|
| `express` | `[body, options?]` | required |
|
|
134
139
|
| `catch`, `listening`, `resume` | `[options?]` | required |
|
|
135
140
|
| `listen`, `ignore`, `unlisten` | `[target, options?]` | required |
|
|
136
141
|
| `hold`, `done` | `[body?, options?]` | required |
|
|
142
|
+
| `deferDelivery` | `[{match, durationMs}]` | required |
|
|
143
|
+
| `renewDeliveryDeferral` | `[id, durationMs]` | required |
|
|
144
|
+
| `releaseDeliveryDeferral` | `[id]` | required |
|
|
137
145
|
|
|
138
146
|
Results are the concrete public operation results, including requested observations. Participant
|
|
139
147
|
handles serialize as names for `participant` and `join.participant` and become real local handles
|
|
140
|
-
on the client
|
|
141
|
-
|
|
148
|
+
on the client, using their retained connection-local references. Done/takeover/rejoin fences old
|
|
149
|
+
references even when the same Body later reacquires the name. Observation locations are canonical
|
|
150
|
+
remote URIs and automatic delivery is false. SDK/stdio deferrals have identical validation, independent
|
|
151
|
+
lease and owner-generation semantics; create/renew return only `{id, expiresAt}`, renew returns null
|
|
152
|
+
and release false for an inactive ID, and stale handles receive the normal ownership refusal.
|
|
153
|
+
No CLI/MCP/Pi deferral-authoring command is included. Errors are JSON-RPC `{code:-32000,message,data:{code?,facts?,operation?}}`; Square codes, structured
|
|
142
154
|
facts, history bounds and express certainty survive the boundary. The client does not infer
|
|
143
155
|
`unsent` from a connection failure. There is no mutation retry or replay.
|
|
144
156
|
|
|
@@ -147,7 +159,8 @@ Progress is a notification
|
|
|
147
159
|
`OperationProgress`. Request abort sends `$/cancelRequest` with `{id}`; the server aborts only that
|
|
148
160
|
request and returns its response **after** cleanup, including releasing a catch lease. Another
|
|
149
161
|
request on the same handle continues normally. Cancellation never undoes an explicitly committed
|
|
150
|
-
receipt.
|
|
162
|
+
receipt or lease mutation. A committed deferral remains until release, expiry or seat retirement;
|
|
163
|
+
connection close does not cancel it. `square/close` with `{}` aborts/drains outstanding requests, closes SQLite and acknowledges
|
|
151
164
|
with a null result. Client close is idempotent, waits briefly for graceful cleanup, then terminates
|
|
152
165
|
an unresponsive process. EOF, malformed framing, version mismatch and process exit reject pending
|
|
153
166
|
requests and release the connection. A lost express response after dispatch has unknown effect;
|
package/guides/change-waits.md
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
# Waiting for changes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Artifact revisions and the rebuildable discovery index drive change detection. Filesystem
|
|
4
|
+
notifications are hints, not activity delivery or consumption receipts.
|
|
4
5
|
|
|
5
|
-
- A process shares a revision reader per canonical artifact and a filesystem watcher per containing
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
- A process shares a revision reader per canonical artifact and a filesystem watcher per containing
|
|
7
|
+
directory. Subscribers have independent baselines, deadlines and cancellation. The final release
|
|
8
|
+
closes the watcher, reconciliation timer and outstanding read.
|
|
9
|
+
- Directory notifications invalidate lightweight reads. A two-second reconciliation checks revisions
|
|
10
|
+
even when notifications are missing. Missing directories, unsupported watches, watcher failures
|
|
11
|
+
and directory replacement retain that fallback. Latency is not a hard guarantee: a missed hint
|
|
12
|
+
may wait for reconciliation, scheduling and lock contention.
|
|
13
|
+
- Conversation revision controls the pending-content cache. Delivery revision separately invalidates
|
|
14
|
+
eligibility, including delivery evidence, cancellation and deferral writes. Projections resolve
|
|
15
|
+
current typed seats and selected bodies in one coherent artifact read; they never grant authority
|
|
16
|
+
from an index entry or a cached eligibility decision. No sidecar, broker or second delivery store
|
|
17
|
+
is introduced. An unreadable candidate cannot authorize cached content.
|
|
18
|
+
- Each active session pending observer subscribes before selecting current candidates and facts,
|
|
19
|
+
including when no seat is yet discoverable. Index publication can wake discovery without an
|
|
20
|
+
artifact write. A failed injection waits for substantive change; its own failed evidence write
|
|
21
|
+
does not cause a retry loop.
|
|
22
|
+
- Deferral create/renew/release and opportunistic pruning change delivery revision, not conversation
|
|
23
|
+
activity. Release rechecks eligibility without a new message. Expiry needs no synthetic write:
|
|
24
|
+
the observer includes the earliest relevant matching live deferral expiry in its existing wait
|
|
25
|
+
deadline, alongside catch-lease expiry/staleness. Overlapping leases remain OR; an early expiry
|
|
26
|
+
rechecks the remaining live leases. Renewal changes that deadline without self-triggered delivery.
|
|
27
|
+
- There is no new daemon or detached per-message timer. A one-shot producer skips deferred work and
|
|
28
|
+
returns promptly. An existing active delivery observer will reconsider release/expiry; absent an
|
|
29
|
+
observer, pending work waits for the next normal delivery opportunity. A lease does not create a
|
|
30
|
+
process that was not already running.
|
|
31
|
+
- Blocking catch waits for conversation revision or its heartbeat/quiet deadline. A held catch keeps
|
|
32
|
+
its single lease alive and pauses quiet expiry. Core catch and streams use the same storage change
|
|
33
|
+
detector. Held express also waits for changes; throttling waits for its eligibility time.
|
|
34
|
+
- Deferrals are independent of catch filters, presentation claims, full-delivery seen receipts,
|
|
35
|
+
clipped previews and session-local cancellation. Sharing storage detection does not share session
|
|
36
|
+
delivery state. Cancellation never creates a receipt or removes archive history. A deferral cannot
|
|
37
|
+
recall a send already past its final freshness check.
|
|
12
38
|
|
|
13
39
|
## Verification
|
|
14
40
|
|
|
@@ -23,6 +49,9 @@ node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 30
|
|
|
23
49
|
node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 300 --mode session --active --no-hints
|
|
24
50
|
```
|
|
25
51
|
|
|
26
|
-
The benchmark creates disposable synthetic history.
|
|
27
|
-
|
|
28
|
-
|
|
52
|
+
The benchmark creates disposable synthetic history. Process-local sharing does not deduplicate
|
|
53
|
+
monitors across processes. Timings measure committed-write-to-projection latency, not native
|
|
54
|
+
message landing, and discovery deadlines are probe bounds rather than an SLA. Historical benchmark
|
|
55
|
+
evidence may describe older HostLedger implementations; it is not current custody authority.
|
|
56
|
+
A long-running extension must reload upgraded code before using the new artifact format; this
|
|
57
|
+
delivery does not install or restart one.
|
package/guides/claude-native.md
CHANGED
|
@@ -21,14 +21,14 @@ An absent/broken endpoint is unavailable; it does not consume Square activity.
|
|
|
21
21
|
|
|
22
22
|
## Native-first routing and policy holds
|
|
23
23
|
|
|
24
|
-
Native delivery is preferred, including under Paseo.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
Native delivery is preferred, including under Paseo. Codex proposes its capable
|
|
25
|
+
native queue before Paseo. Claude and OpenCode receivers register their native
|
|
26
|
+
routes only through their loaded integrations. The common publication transaction
|
|
27
|
+
keeps a fresh native registration ahead of a Paseo proposal for the same
|
|
28
|
+
participant, session, channel and ownership epoch. Tool operations cannot flip it
|
|
29
|
+
back to Paseo between receiver reconciliations. An absent, expired or explicitly
|
|
30
|
+
retired native registration does not block a Paseo fallback; a replacement epoch
|
|
31
|
+
cannot inherit an older native registration.
|
|
32
32
|
|
|
33
33
|
This is route preference, not permission to bypass receiver policy. Claude can
|
|
34
34
|
legitimately hold an unsolicited peer message with `no-mode-asserted`, especially
|
|
@@ -47,7 +47,7 @@ Neither a release nor a drop is a queue receipt, proof of consumption or retry
|
|
|
47
47
|
authorization.
|
|
48
48
|
|
|
49
49
|
`square harness doctor delivery` shows native custody separately from pending
|
|
50
|
-
attention
|
|
50
|
+
attention, including after the activity is seen or the participant leaves. Paseo
|
|
51
51
|
presents the `peer_message_hold` state in its own timeline. Acceptance means normal
|
|
52
52
|
delivery when the receiver allows it, or a visible reason without bypass/replay when
|
|
53
53
|
policy holds it.
|
package/guides/mcp.md
CHANGED
|
@@ -4,7 +4,7 @@ Square provides a stdio MCP server through `square mcp-server`.
|
|
|
4
4
|
|
|
5
5
|
Set `SQUARE_LOCATION` to the default `.square` artifact path; calls may override it with `location`. Relative paths use the server's startup working directory. Join with an explicit `as` name. Later calls may omit `as` only when the artifact recognizes exactly one current name held by the call's exact Body (`channel`, `session`, optional `agent`); zero or multiple matches refuse and require explicit selection. An explicit name selects a voice but never grants custody. Request metadata supplies the live Codex (`threadId`) or OpenCode (`ai.opencode/sessionID`) coordinate when present; otherwise a reliable startup coordinate is required for participant writes. Read-only calls do not need a Body.
|
|
6
6
|
|
|
7
|
-
The server exposes `join`, `express`, `catch`, `history`, `
|
|
7
|
+
The server exposes `join`, `express`, `catch`, `history`, `listen`, `ignore`, `listening`, `hold`, `resume`, `done`, `status`, and `participants`. `catch` consumes activities. `history` is read-only and accepts stable `act/<index>` cursors. Tool results include JSON `structuredContent` and a text representation of the same data. Square failures are returned as tool errors with a structured `{ error: { code, message, facts? } }` value.
|
|
8
8
|
|
|
9
9
|
Claude's bundled plugin configures this server automatically. Its mod supplies live session/cwd/subagent attribution through an out-of-band tool-use-ID association, not AI-visible arguments; see [Claude call identity](claude-native.md#mcp-tools-and-call-identity). The bundled endpoint requires that association rather than falling back to its startup environment.
|
|
10
10
|
|
package/guides/migration.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Square stores its authority in one versioned SQLite artifact. Format 1 kept the
|
|
4
4
|
history as one JSON snapshot; format 2 introduced typed rows; format 3 added the
|
|
5
|
-
`unlisten` activity; format 4
|
|
6
|
-
evidence. Existing artifacts are never
|
|
5
|
+
`unlisten` activity; format 4 added artifact-owned participant seats and delivery
|
|
6
|
+
evidence; format 5 adds independent expiring delivery deferrals. Existing artifacts are never
|
|
7
|
+
converted implicitly.
|
|
7
8
|
|
|
8
9
|
Use the explicit command while all processes using the artifact are stopped:
|
|
9
10
|
|
|
@@ -11,11 +12,24 @@ Use the explicit command while all processes using the artifact are stopped:
|
|
|
11
12
|
square --location /path/to/SQUARE.square migrate
|
|
12
13
|
```
|
|
13
14
|
|
|
14
|
-
The command handles
|
|
15
|
+
The command handles supported formats 2, 3 and 4 in place and publishes only final format 5.
|
|
16
|
+
It does not create a
|
|
15
17
|
backup, stop processes, install a runtime, or update another artifact. A legacy
|
|
16
18
|
HostLedger that needs its own format conversion must be prepared separately with
|
|
17
19
|
the old runtime's offline tooling.
|
|
18
20
|
|
|
21
|
+
## Format 4 additive conversion
|
|
22
|
+
|
|
23
|
+
An exact format-4 artifact needs no legacy owner or route import: that authority already lives
|
|
24
|
+
inside the artifact. Read-only preflight checks framing and stored facts. One SQLite transaction
|
|
25
|
+
adds the empty `delivery_deferrals` table and its owner/generation/expiry lookup index, then
|
|
26
|
+
publishes format 5. The dedicated table stores participant key, captured generation, opaque ID,
|
|
27
|
+
validated match and expiry, separately from catch leases and delivery evidence. All existing
|
|
28
|
+
activities (including metadata), audience, receipts, seats, catch leases, delivery evidence,
|
|
29
|
+
cancellation boundaries and both revision values remain unchanged. No lease is manufactured.
|
|
30
|
+
The conversion does not consult or mutate HostLedger. Failure rolls back the table, index and
|
|
31
|
+
format marker together.
|
|
32
|
+
|
|
19
33
|
## Format 3 preflight and conversion
|
|
20
34
|
|
|
21
35
|
Before writing, migration validates the exact format-3 schema, header, and legacy
|
|
@@ -54,8 +68,8 @@ ledger is read-only and remains untouched, so it is still available for offline
|
|
|
54
68
|
diagnostics. An absent ledger cannot establish standing ownership; an unreadable
|
|
55
69
|
or unsupported ledger refuses before changing the square.
|
|
56
70
|
|
|
57
|
-
After successful preflight, one SQLite transaction creates
|
|
58
|
-
|
|
71
|
+
After successful preflight, one SQLite transaction creates ownership/evidence rows and the
|
|
72
|
+
empty deferral table/index, then publishes format 5. Activity history and indices,
|
|
59
73
|
receipts, lease contents, and the artifact revision are preserved. Legacy routes
|
|
60
74
|
are decoded only during migration and their table is removed. Existing lease
|
|
61
75
|
generations are resolved only for exact owner spellings that match a standing
|
|
@@ -65,12 +79,13 @@ format, and any failure before commit rolls back the conversion.
|
|
|
65
79
|
|
|
66
80
|
Format 2 first rebuilds only the activity table and its indexes to accept
|
|
67
81
|
`unlisten`, then follows the same row-format preflight and conversion. Snapshot
|
|
68
|
-
format 1 is not accepted by this format
|
|
82
|
+
format 1 is not accepted by this migration. Formats 2/3 never publish an intermediate format 4.
|
|
69
83
|
|
|
70
84
|
## What migration refuses
|
|
71
85
|
|
|
72
|
-
Migration validates
|
|
73
|
-
using migration-local codecs
|
|
86
|
+
Migration validates each source's exact row schema. Formats 2/3 include the legacy routes table,
|
|
87
|
+
read using migration-local codecs; format 4 already has seats and delivery evidence. Current
|
|
88
|
+
format 5 has no routes table or runtime route
|
|
74
89
|
reader; migration consumes the old rows only to resolve seats, then drops the legacy
|
|
75
90
|
table as part of the format change.
|
|
76
91
|
|
|
@@ -99,8 +114,8 @@ remains a byte-preserving no-op.
|
|
|
99
114
|
|
|
100
115
|
## Before and after migration
|
|
101
116
|
|
|
102
|
-
Stop every session or service using the square and any process writing its
|
|
103
|
-
HostLedger. Make an offline backup, or use SQLite's backup facilities rather than
|
|
117
|
+
Stop every session or service using the square, and for formats 2/3 freeze any process writing its
|
|
118
|
+
legacy HostLedger. Make an offline backup, or use SQLite's backup facilities rather than
|
|
104
119
|
copying a live database. Migration does not revoke an in-flight remote delivery or
|
|
105
120
|
modify HostLedger evidence. Square writes use ordinary SQLite `DELETE` journals:
|
|
106
121
|
the artifact must be writable and its containing directory must permit journal
|
|
@@ -109,7 +124,10 @@ participation and delivery authority live in the artifact, not writable HOME;
|
|
|
109
124
|
loss of the rebuildable HOME discovery index may limit automatic discovery but
|
|
110
125
|
does not transfer custody or erase durable delivery evidence.
|
|
111
126
|
|
|
112
|
-
Upgrade attached runtimes before reopening the format-
|
|
113
|
-
must not be mixed with it.
|
|
127
|
+
Upgrade all attached runtimes before reopening the format-5 artifact; older runtimes
|
|
128
|
+
must not be mixed with it. Updating a package does not migrate an artifact, and migrating an
|
|
129
|
+
artifact does not install upgraded runtimes. For rollback after a committed conversion, stop all
|
|
130
|
+
clients and restore the offline backup with its compatible older runtime; do not downgrade the
|
|
131
|
+
format marker or drop tables in place. Run the read-only `doctor` command after migration and
|
|
114
132
|
before restarting clients. Repeating migration on a current artifact is a
|
|
115
133
|
byte-preserving no-op.
|