@astrosheep/square 0.3.93 → 0.3.95
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/activity-feed.d.ts +3 -5
- package/dist/activity-feed.js +16 -14
- package/dist/artifact.js +3 -3
- package/dist/attention.d.ts +19 -0
- package/dist/attention.js +30 -0
- package/dist/boundary-presentation.d.ts +2 -2
- package/dist/boundary-presentation.js +33 -40
- package/dist/catch-decisions.d.ts +5 -8
- package/dist/catch-decisions.js +45 -25
- package/dist/claude-delivery-status.js +10 -4
- package/dist/claude-delivery.js +19 -11
- package/dist/claude-mod.js +24 -53
- package/dist/cli/context.d.ts +9 -4
- package/dist/cli/context.js +30 -27
- package/dist/cli/express-presentation.d.ts +3 -1
- package/dist/cli/express-presentation.js +28 -16
- package/dist/cli/json.d.ts +3 -0
- package/dist/cli/json.js +14 -0
- package/dist/cli/mcp-server-command.js +2 -2
- package/dist/cli/observation-commands.d.ts +1 -3
- package/dist/cli/observation-commands.js +43 -56
- package/dist/cli/program.js +3 -3
- package/dist/cli/registry.js +7 -6
- package/dist/cli/square-commands.d.ts +0 -2
- package/dist/cli/square-commands.js +30 -44
- package/dist/codex-hook.d.ts +3 -2
- package/dist/codex-hook.js +13 -4
- package/dist/codex-queue.js +4 -4
- package/dist/decisions.d.ts +5 -16
- package/dist/decisions.js +9 -33
- package/dist/delivery-attempts.d.ts +52 -0
- package/dist/delivery-attempts.js +73 -0
- package/dist/delivery-deferral.js +2 -2
- package/dist/delivery-effects.d.ts +139 -0
- package/dist/delivery-effects.js +163 -0
- package/dist/delivery-health.d.ts +0 -1
- package/dist/delivery-health.js +1 -3
- package/dist/delivery-operations.d.ts +2 -2
- package/dist/delivery-operations.js +76 -101
- package/dist/delivery-primitives.d.ts +5 -0
- package/dist/delivery-primitives.js +3 -0
- package/dist/delivery.d.ts +1 -1
- package/dist/delivery.js +7 -6
- package/dist/help.js +14 -14
- package/dist/http-delivery.d.ts +16 -0
- package/dist/http-delivery.js +101 -0
- package/dist/inbox.d.ts +9 -22
- package/dist/inbox.js +23 -27
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -1
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +23 -24
- package/dist/model.d.ts +12 -21
- package/dist/model.js +5 -8
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.js +8 -5
- package/dist/operation-presentation.d.ts +6 -6
- package/dist/operation-presentation.js +55 -58
- package/dist/owner.d.ts +11 -0
- package/dist/owner.js +32 -0
- package/dist/paseo-delivery.d.ts +4 -1
- package/dist/paseo-delivery.js +7 -12
- package/dist/paseo-state.d.ts +2 -2
- package/dist/paseo-state.js +6 -4
- package/dist/plugins/opencode/delivery.js +27 -15
- package/dist/ports.d.ts +18 -47
- package/dist/presentation-operations.d.ts +14 -19
- package/dist/presentation-operations.js +51 -61
- package/dist/presentation.d.ts +7 -17
- package/dist/presentation.js +46 -91
- package/dist/receive.d.ts +66 -0
- package/dist/receive.js +54 -0
- package/dist/runtime.d.ts +1 -8
- package/dist/runtime.js +2 -17
- package/dist/seats.d.ts +2 -3
- package/dist/seats.js +7 -15
- package/dist/session-lifecycle.d.ts +6 -2
- package/dist/session-lifecycle.js +39 -11
- package/dist/sqlite-square.d.ts +36 -23
- package/dist/sqlite-square.js +162 -211
- package/dist/square-api.d.ts +16 -11
- package/dist/square-api.js +27 -23
- package/dist/square-core.d.ts +0 -14
- package/dist/square-core.js +0 -9
- package/dist/square-domain.d.ts +4 -2
- package/dist/square-domain.js +65 -52
- package/dist/square-facade.d.ts +47 -105
- package/dist/square-index.d.ts +5 -13
- package/dist/square-index.js +13 -38
- package/dist/square-legacy-framing.d.ts +2 -0
- package/dist/square-legacy-framing.js +8 -0
- package/dist/square-location.d.ts +1 -14
- package/dist/square-location.js +4 -70
- package/dist/square-migration-host-ledger.d.ts +2 -2
- package/dist/square-migration-host-ledger.js +6 -5
- package/dist/square-migration.d.ts +4 -1
- package/dist/square-migration.js +93 -38
- package/dist/square-receive.d.ts +6 -0
- package/dist/square-receive.js +18 -0
- package/dist/square-sqlite.d.ts +9 -3
- package/dist/square-sqlite.js +139 -114
- package/dist/square-tools.d.ts +7 -10
- package/dist/square-tools.js +21 -61
- package/dist/stream.js +4 -5
- package/dist/views.d.ts +23 -13
- package/dist/views.js +47 -53
- package/dist/wakes.d.ts +1 -5
- package/dist/wakes.js +4 -6
- package/examples/symmetric-domains.mjs +250 -0
- package/guides/application-facade-delivery-evidence.md +6 -6
- package/guides/attention-primitive.md +32 -0
- package/guides/change-waits.md +2 -2
- package/guides/claude-native.md +18 -9
- package/guides/codex-delivery.md +17 -6
- package/guides/delivery-primitives.md +180 -0
- package/guides/mcp-connection-lifetime.md +1 -1
- package/guides/mcp.md +3 -3
- package/guides/migration.md +148 -151
- package/guides/operation-cancellation.md +1 -1
- package/guides/participant-ownership.md +78 -47
- package/guides/pi-tools.md +34 -5
- package/guides/primitive-catch-consolidation.md +5 -5
- package/guides/primitive-history-consolidation.md +2 -2
- package/guides/primitive-status-consolidation.md +3 -3
- package/guides/primitive-storage-consolidation.md +1 -1
- package/guides/receive-primitive.md +90 -0
- package/guides/row-runtime-delivery-evidence.md +1 -1
- package/guides/row-storage-design.md +22 -28
- package/guides/square-api.md +314 -91
- package/guides/symmetric-domains.md +92 -0
- package/package.json +14 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +2 -6
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +27 -17
- package/plugins/paseo/client/palette.ts +18 -18
- package/plugins/paseo/client/square-message.tsx +17 -23
- package/plugins/pi/square-pi.js +17 -17
- package/skills/square/SKILL.md +2 -6
- package/dist/square-backend.d.ts +0 -13
- package/dist/square-backend.js +0 -1
- package/dist/square-stdio.d.ts +0 -22
- package/dist/square-stdio.js +0 -235
- package/dist/stdio-server.d.ts +0 -3
- package/dist/stdio-server.js +0 -212
- package/guides/backends.md +0 -193
package/dist/presentation.js
CHANGED
|
@@ -4,7 +4,6 @@ import path from 'node:path';
|
|
|
4
4
|
import { participantIdentity } from './participant-identity.js';
|
|
5
5
|
export { participantIdentity } from './participant-identity.js';
|
|
6
6
|
import { formatActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
|
|
7
|
-
import { HELD_WAIT_BUDGET_MS } from './runtime.js';
|
|
8
7
|
import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
|
|
9
8
|
import { grepSnippet } from './search.js';
|
|
10
9
|
import { style } from './tty-style.js';
|
|
@@ -27,8 +26,7 @@ export function displayPath(squarePath, cwd = process.cwd()) {
|
|
|
27
26
|
}
|
|
28
27
|
export function withPathOutput(squarePath, body = '', opts = {}) {
|
|
29
28
|
const count = opts.participantCount ?? 0;
|
|
30
|
-
const
|
|
31
|
-
const line = `· ${displayPath(squarePath)} — ${count} in the square${heldSuffix}`;
|
|
29
|
+
const line = `· ${displayPath(squarePath)} — ${count} in the square`;
|
|
32
30
|
return [opts.plain === true ? line : style('dim', line), ...(body === '' ? [] : ['', body])].join('\n') + '\n';
|
|
33
31
|
}
|
|
34
32
|
/** The same header/blank/body layout as withPathOutput, for refusal bodies written to stderr. */
|
|
@@ -50,18 +48,18 @@ export function quoteShell(value) {
|
|
|
50
48
|
export function commandPrefix(squarePath) {
|
|
51
49
|
return `square --location ${quoteShell(squarePath)}`;
|
|
52
50
|
}
|
|
53
|
-
export function participantCommandPrefix(squarePath, name) {
|
|
54
|
-
return `square --location ${quoteShell(/^[A-Za-z][A-Za-z0-9+.-]*:/.test(squarePath) ? squarePath : path.resolve(squarePath))} --as ${quoteShell(name)}`;
|
|
51
|
+
export function participantCommandPrefix(squarePath, name, owner) {
|
|
52
|
+
return `square --location ${quoteShell(/^[A-Za-z][A-Za-z0-9+.-]*:/.test(squarePath) ? squarePath : path.resolve(squarePath))} --as ${quoteShell(name)}${owner === undefined ? '' : ` --owner ${quoteShell(owner)}`}`;
|
|
55
53
|
}
|
|
56
54
|
/** Blocked actions end with a full copy-pasteable recovery command. */
|
|
57
|
-
export function joinRecoveryCommand(squarePath, name) {
|
|
58
|
-
return `${participantCommandPrefix(squarePath, name)} join`;
|
|
55
|
+
export function joinRecoveryCommand(squarePath, name, owner) {
|
|
56
|
+
return `${participantCommandPrefix(squarePath, name, owner)} join`;
|
|
59
57
|
}
|
|
60
58
|
/** Taking a name is an explicit recovery choice, never an automatic retry. */
|
|
61
|
-
export function takeoverRecoveryLines(squarePath, name) {
|
|
59
|
+
export function takeoverRecoveryLines(squarePath, name, owner) {
|
|
62
60
|
return [
|
|
63
61
|
'If this is your name to reclaim, --kick banishes the one standing here so you can step in:',
|
|
64
|
-
`${participantCommandPrefix(squarePath, name)} join --kick`,
|
|
62
|
+
`${participantCommandPrefix(squarePath, name, owner)} join --kick`,
|
|
65
63
|
];
|
|
66
64
|
}
|
|
67
65
|
export function participantsRecoveryCommand(squarePath) {
|
|
@@ -79,13 +77,14 @@ export function renderPresenceLines(recent, now, limit = 5) {
|
|
|
79
77
|
if (shown.length === 0)
|
|
80
78
|
return [style('dim', ' ○ nobody nearby')];
|
|
81
79
|
const lines = shown.map((p) => {
|
|
82
|
-
const glyph = p.
|
|
83
|
-
const text = p.
|
|
80
|
+
const glyph = p.presence === 'done' || p.presence === 'away' ? '○' : p.presence === 'watching' ? '◎' : p.lastActiveAt !== undefined ? '●' : '○';
|
|
81
|
+
const text = p.presence === 'done'
|
|
84
82
|
? (p.lastActiveAt === undefined ? 'stepped out of the square' : `stepped out of the square · ${formatRelativeTime(p.lastActiveAt, now)}`)
|
|
85
|
-
: p.presence === '
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
: p.
|
|
83
|
+
: p.presence === 'away' ? 'is away'
|
|
84
|
+
: p.presence === 'watching'
|
|
85
|
+
? (p.lastActiveAt === undefined ? 'is nearby · catching' : `is nearby · catching · ${formatRelativeTime(p.lastActiveAt, now)}`)
|
|
86
|
+
: p.lastActiveAt === undefined ? 'is nearby · quiet'
|
|
87
|
+
: p.activityCount > 0 ? `is nearby · ${formatRelativeTime(p.lastActiveAt, now)}` : `is nearby · quiet · ${formatRelativeTime(p.lastActiveAt, now)}`;
|
|
89
88
|
return ` ${glyph === '○' ? style('dim', glyph) : glyph} ${participantIdentity(p.name)}${style('dim', ` ${text}`)}`;
|
|
90
89
|
});
|
|
91
90
|
const remaining = recent.length - shown.length;
|
|
@@ -122,10 +121,6 @@ export function renderRoomChangeText(event) {
|
|
|
122
121
|
return `${identity} stepped into the square`;
|
|
123
122
|
case 'done':
|
|
124
123
|
return `${identity} stepped out of the square`;
|
|
125
|
-
case 'hold':
|
|
126
|
-
return `${identity} raised a hand${event.body ? ` — ${event.body}` : ''}`;
|
|
127
|
-
case 'resume':
|
|
128
|
-
return `${identity} lowered the hand`;
|
|
129
124
|
case 'listen':
|
|
130
125
|
return `${identity} turned an ear toward ${participantIdentity(event.target ?? 'someone')}`;
|
|
131
126
|
case 'ignore':
|
|
@@ -158,10 +153,6 @@ function renderEventCliFacts(event, opts = {}) {
|
|
|
158
153
|
switch (event.kind) {
|
|
159
154
|
case 'join':
|
|
160
155
|
return { text: `· ${renderRoomChangeText(event)}`, bodyClipped: false };
|
|
161
|
-
case 'hold':
|
|
162
|
-
return { text: `· ${renderRoomChangeText(event)}`, bodyClipped: false };
|
|
163
|
-
case 'resume':
|
|
164
|
-
return { text: `${style('release', '✓')} ${renderRoomChangeText(event)}`, bodyClipped: false };
|
|
165
156
|
case 'listen':
|
|
166
157
|
case 'ignore':
|
|
167
158
|
case 'unlisten':
|
|
@@ -229,42 +220,31 @@ function draftSavedLines(draftPath) {
|
|
|
229
220
|
function withDraftInput(command, draftPath) {
|
|
230
221
|
return draftPath === undefined ? command : `${command} < ${quoteShell(draftPath)}`;
|
|
231
222
|
}
|
|
232
|
-
function renderUnreadSummary(
|
|
233
|
-
const visibleSummaries =
|
|
234
|
-
const remainingSummaries =
|
|
223
|
+
function renderUnreadSummary(activitySummaries) {
|
|
224
|
+
const visibleSummaries = activitySummaries.slice(0, MAX_IDENTITY_SET_SIZE);
|
|
225
|
+
const remainingSummaries = activitySummaries.length - visibleSummaries.length;
|
|
235
226
|
return [
|
|
236
|
-
...visibleSummaries.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const compact = (preview.act.body ?? '').replace(/\s+/g, ' ').trim();
|
|
245
|
-
const bodyPreview = compact === ''
|
|
246
|
-
? { text: preview.act.metadata === undefined ? '(empty)' : '(structured activity)', remaining: 0 }
|
|
247
|
-
: truncateChars(compact, UNREAD_PREVIEW_CHARS);
|
|
248
|
-
const bodyText = bodyPreview.remaining === 0 ? bodyPreview.text : `${bodyPreview.text}${style('dim', `… (+${bodyPreview.remaining} chars)`)}`;
|
|
249
|
-
return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${bodyText}"`);
|
|
250
|
-
}),
|
|
251
|
-
]),
|
|
227
|
+
...visibleSummaries.map((item) => {
|
|
228
|
+
const compact = (item.preview.body ?? '').replace(/\s+/g, ' ').trim();
|
|
229
|
+
const bodyPreview = compact === ''
|
|
230
|
+
? { text: item.preview.metadata === undefined ? '(empty)' : '(structured activity)', remaining: 0 }
|
|
231
|
+
: truncateChars(compact, UNREAD_PREVIEW_CHARS);
|
|
232
|
+
const bodyText = bodyPreview.remaining === 0 ? bodyPreview.text : `${bodyPreview.text}${style('dim', `… (+${bodyPreview.remaining} chars)`)}`;
|
|
233
|
+
return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${bodyText}"`);
|
|
234
|
+
}),
|
|
252
235
|
...(remainingSummaries === 0 ? [] : [style('dim', ` · ${remainingSummaries} more participants have unread activity`)]),
|
|
253
236
|
];
|
|
254
237
|
}
|
|
255
|
-
export function renderPendingFeed(publicItems, sayNumbers, perceptions, viewer) {
|
|
256
|
-
return publicItems.map((act) => renderAmbientEvent(act, viewer, { actNumber: sayNumbers[act.index], perception: perceptions[act.index] })).filter(Boolean).join('\n\n');
|
|
257
|
-
}
|
|
258
238
|
export function renderActivityBlocked(opts) {
|
|
259
|
-
const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} catch`;
|
|
239
|
+
const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} catch`;
|
|
260
240
|
return withPathOutput(opts.squarePath, [
|
|
261
241
|
`${style('blocked', '✕')} your activity hasn't landed — someone spoke while you were away`,
|
|
262
|
-
...renderUnreadSummary(
|
|
242
|
+
...renderUnreadSummary(opts.activitySummaries),
|
|
263
243
|
...draftSavedLines(opts.draftPath),
|
|
264
244
|
`${readNowCommand}`,
|
|
265
245
|
' · take it in, then retry:',
|
|
266
246
|
withDraftInput(opts.retryCommand, opts.draftPath),
|
|
267
|
-
].join('\n'), { participantCount: opts.participantCount
|
|
247
|
+
].join('\n'), { participantCount: opts.participantCount });
|
|
268
248
|
}
|
|
269
249
|
/** An indeterminate or already-committed send only points to inspection, never resend. */
|
|
270
250
|
export function renderActivityUncertain(opts) {
|
|
@@ -278,47 +258,22 @@ export function renderActivityUncertain(opts) {
|
|
|
278
258
|
`${commandPrefix(opts.squarePath)} history${opts.landedId === undefined ? ' --limit 10' : ` --at ${opts.landedId}`} --no-truncate`,
|
|
279
259
|
], opts);
|
|
280
260
|
}
|
|
281
|
-
/** A blank or whitespace hold reason is absent, never an empty sensory line. */
|
|
282
|
-
function holdReasonLines(reason) {
|
|
283
|
-
const text = reason?.trim() ?? '';
|
|
284
|
-
return text === '' ? [] : [style('dim', ` · ${text}`)];
|
|
285
|
-
}
|
|
286
|
-
function heldMainLine(holder, styled) {
|
|
287
|
-
const glyph = styled ? style('blocked', '✕') : '·';
|
|
288
|
-
const who = holder === undefined ? 'a hand is raised' : `${participantIdentity(holder)} has a hand raised`;
|
|
289
|
-
return `${glyph} ${who} — voices drop, yours too`;
|
|
290
|
-
}
|
|
291
261
|
export function renderExpressWaiting(opts) {
|
|
292
|
-
if (opts.reason === 'throttled') {
|
|
293
|
-
return [
|
|
294
|
-
'· the square is packed — your activity is waiting',
|
|
295
|
-
` · a lull opens in ${formatDuration(opts.delayMs)}`,
|
|
296
|
-
' · --no-wait saves a draft and returns now',
|
|
297
|
-
].join('\n');
|
|
298
|
-
}
|
|
299
262
|
return [
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
` · your activity is waiting — after ${formatDuration(HELD_WAIT_BUDGET_MS)} it saves a draft and stops`,
|
|
263
|
+
'· the square is packed — your activity is waiting',
|
|
264
|
+
` · a lull opens in ${formatDuration(opts.delayMs)}`,
|
|
303
265
|
' · --no-wait saves a draft and returns now',
|
|
304
266
|
].join('\n');
|
|
305
267
|
}
|
|
306
268
|
export function renderExpressNoWait(opts) {
|
|
307
269
|
const retryCommand = opts.retryCommand;
|
|
308
|
-
const lines =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
: [
|
|
316
|
-
heldMainLine(opts.holder, true),
|
|
317
|
-
...holdReasonLines(opts.holdReason),
|
|
318
|
-
...draftSavedLines(opts.draftPath),
|
|
319
|
-
`${withDraftInput(retryCommand, opts.draftPath)}`,
|
|
320
|
-
];
|
|
321
|
-
return withPathOutput(opts.squarePath, lines.join('\n'), { participantCount: opts.participantCount, held: opts.held });
|
|
270
|
+
const lines = [
|
|
271
|
+
`${style('blocked', '✕')} the square is packed — shoulder to shoulder`,
|
|
272
|
+
` · a lull opens in ${formatDuration(opts.delayMs)}`,
|
|
273
|
+
...draftSavedLines(opts.draftPath),
|
|
274
|
+
`${withDraftInput(retryCommand, opts.draftPath)}`,
|
|
275
|
+
];
|
|
276
|
+
return withPathOutput(opts.squarePath, lines.join('\n'), { participantCount: opts.participantCount });
|
|
322
277
|
}
|
|
323
278
|
export function renderPresenceAnchor(names) {
|
|
324
279
|
const visibleNames = names.slice(0, MAX_IDENTITY_SET_SIZE);
|
|
@@ -337,7 +292,7 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
|
|
|
337
292
|
let truncated = false;
|
|
338
293
|
for (const act of publicVisible) {
|
|
339
294
|
const rawBody = act.body ?? '';
|
|
340
|
-
const header = `${style('dim', formatActivityId(act.index))} · ${
|
|
295
|
+
const header = `${style('dim', formatActivityId(act.index))} · ${participantIdentity(act.actor)} · ${style('dim', formatTimestamp(act.at))}`;
|
|
341
296
|
if (perception?.(act) === 'presence') {
|
|
342
297
|
chunks.push(header);
|
|
343
298
|
continue;
|
|
@@ -373,20 +328,20 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
|
|
|
373
328
|
export function renderActivityLimit(opts) {
|
|
374
329
|
// The cap wording lives inside the count: the speaker is simply spent.
|
|
375
330
|
const spoken = opts.count !== undefined && opts.hardCap !== undefined ? ` — ${opts.count}/${opts.hardCap} spoken` : '';
|
|
376
|
-
const doneCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} done`;
|
|
331
|
+
const doneCommand = `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} done`;
|
|
377
332
|
return withPathOutput(opts.squarePath, [
|
|
378
333
|
`${style('blocked', '✕')} you’ve reached your speaking cap${spoken}`,
|
|
379
334
|
...draftSavedLines(opts.draftPath),
|
|
380
335
|
' · your draft stays unsent; done only steps out',
|
|
381
336
|
doneCommand,
|
|
382
|
-
].join('\n'), { participantCount: opts.participantCount
|
|
337
|
+
].join('\n'), { participantCount: opts.participantCount });
|
|
383
338
|
}
|
|
384
339
|
export function renderWatchAlreadyActive(opts) {
|
|
385
340
|
return [
|
|
386
341
|
`${style('blocked', '✕')} you are already catching`,
|
|
387
342
|
` · an active catch is already running for ${participantIdentity(opts.name)}`,
|
|
388
343
|
' · --replace lets a new catch take over',
|
|
389
|
-
`${participantCommandPrefix(opts.squarePath, opts.name)} catch --idle 30m --replace`,
|
|
344
|
+
`${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} catch --idle 30m --replace`,
|
|
390
345
|
].join('\n');
|
|
391
346
|
}
|
|
392
347
|
export function renderWatchForceTakeover(_opts) {
|
|
@@ -406,9 +361,9 @@ export function renderWatchStatus(opts) {
|
|
|
406
361
|
? ['', 'around the square', ...renderPresenceLines(others, opts.presence.now)]
|
|
407
362
|
: [];
|
|
408
363
|
switch (opts.status) {
|
|
409
|
-
case '
|
|
410
|
-
case '
|
|
411
|
-
const prefix = participantCommandPrefix(opts.squarePath, opts.name);
|
|
364
|
+
case 'idle':
|
|
365
|
+
case 'drained': {
|
|
366
|
+
const prefix = participantCommandPrefix(opts.squarePath, opts.name, opts.owner);
|
|
412
367
|
// Quiet graduates with the idle length; an empty catch has no idle budget to report.
|
|
413
368
|
const idleMs = opts.idleMs;
|
|
414
369
|
const quiet = idleMs !== undefined && idleMs >= 60 * 60 * 1000
|
|
@@ -425,12 +380,12 @@ export function renderWatchStatus(opts) {
|
|
|
425
380
|
].join('\n');
|
|
426
381
|
}
|
|
427
382
|
case 'quorum':
|
|
428
|
-
return [`${style('release', '✓')} everyone else has left — the square is yours alone`, `${participantCommandPrefix(opts.squarePath, opts.name)} done`].join('\n');
|
|
383
|
+
return [`${style('release', '✓')} everyone else has left — the square is yours alone`, `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} done`].join('\n');
|
|
429
384
|
case 'capped': {
|
|
430
385
|
const spoken = opts.hardCap === undefined || opts.hardCap === null
|
|
431
386
|
? ''
|
|
432
387
|
: ` — ${opts.ownActivityCount ?? 0}/${opts.hardCap} spoken`;
|
|
433
|
-
return [`${style('blocked', '✕')} you’ve reached your speaking cap${spoken}`, `${participantCommandPrefix(opts.squarePath, opts.name)} done`].join('\n');
|
|
388
|
+
return [`${style('blocked', '✕')} you’ve reached your speaking cap${spoken}`, `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} done`].join('\n');
|
|
434
389
|
}
|
|
435
390
|
}
|
|
436
391
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Synchronous capabilities borrowed from one caller-owned transaction. */
|
|
2
|
+
export interface Item<K extends string | number, D> {
|
|
3
|
+
readonly key: K;
|
|
4
|
+
readonly data: D;
|
|
5
|
+
}
|
|
6
|
+
export interface Address<V> {
|
|
7
|
+
readonly recipient: string;
|
|
8
|
+
readonly via: V;
|
|
9
|
+
}
|
|
10
|
+
export interface ReceivedHead<K extends string | number, V> {
|
|
11
|
+
readonly key: K;
|
|
12
|
+
readonly via: V;
|
|
13
|
+
}
|
|
14
|
+
export interface ReceiptRef<K extends string | number> {
|
|
15
|
+
readonly recipient: string;
|
|
16
|
+
readonly item: K;
|
|
17
|
+
}
|
|
18
|
+
export type Selection<K extends string | number, Q> = {
|
|
19
|
+
readonly kind: 'page';
|
|
20
|
+
readonly query: Q;
|
|
21
|
+
readonly limit: number;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: 'exact';
|
|
24
|
+
readonly key: K;
|
|
25
|
+
readonly remainingQuery: Q;
|
|
26
|
+
};
|
|
27
|
+
export interface ReceiveRead<K extends string | number, D, V, Q> {
|
|
28
|
+
heads(recipient: string, query: Q, options: {
|
|
29
|
+
readonly unconsumed: boolean;
|
|
30
|
+
readonly limit: number;
|
|
31
|
+
}): readonly ReceivedHead<K, V>[];
|
|
32
|
+
addressed(recipient: string, item: K): ReceivedHead<K, V> | undefined;
|
|
33
|
+
load(keys: readonly K[]): ReadonlyMap<K, D>;
|
|
34
|
+
count(recipient: string, query: Q, unconsumed: boolean): number;
|
|
35
|
+
receipt(ref: ReceiptRef<K>): {
|
|
36
|
+
readonly at: number;
|
|
37
|
+
} | undefined;
|
|
38
|
+
}
|
|
39
|
+
export interface ReceiveWrite<K extends string | number, D, V, Q> extends ReceiveRead<K, D, V, Q> {
|
|
40
|
+
insertItem(item: Item<K, D>): void;
|
|
41
|
+
insertAddress(item: K, address: Address<V>, order: number): void;
|
|
42
|
+
/** True only for a new coordinate; an existing receipt retains its first timestamp. */
|
|
43
|
+
insertReceipt(ref: ReceiptRef<K>, at: number): boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare class ReceiveError<K extends string | number = string | number> extends Error {
|
|
46
|
+
readonly code: 'not_addressed' | 'missing_item';
|
|
47
|
+
readonly key: K;
|
|
48
|
+
constructor(code: 'not_addressed' | 'missing_item', key: K);
|
|
49
|
+
}
|
|
50
|
+
/** Storage rejects duplicate items/addresses; the caller rolls back the whole admission. */
|
|
51
|
+
export declare function receive<K extends string | number, D, V>(tx: Pick<ReceiveWrite<K, D, V, unknown>, 'insertItem' | 'insertAddress'>, input: {
|
|
52
|
+
readonly item: Item<K, D>;
|
|
53
|
+
readonly audience: readonly Address<V>[];
|
|
54
|
+
}): void;
|
|
55
|
+
/** Queries filter before limiting. Exact selection ignores consumption, not addressing. */
|
|
56
|
+
export declare function selectReceivedHeads<K extends string | number, V, Q>(tx: Pick<ReceiveRead<K, unknown, V, Q>, 'heads' | 'addressed'>, recipient: string, selection: Selection<K, Q>): readonly ReceivedHead<K, V>[];
|
|
57
|
+
/** Record captured coordinates without reselecting or checking their current eligibility. */
|
|
58
|
+
export declare function recordConsumed<K extends string | number>(tx: Pick<ReceiveWrite<K, unknown, unknown, unknown>, 'insertReceipt'>, refs: readonly ReceiptRef<K>[], at: number): number;
|
|
59
|
+
/** Selection, hydration, receipts and remaining count must share the caller's transaction. */
|
|
60
|
+
export declare function consumeReceived<K extends string | number, D, V, Q>(tx: Pick<ReceiveWrite<K, D, V, Q>, 'heads' | 'addressed' | 'load' | 'insertReceipt' | 'count'>, recipient: string, selection: Selection<K, Q>, at: number): {
|
|
61
|
+
readonly items: readonly (Item<K, D> & {
|
|
62
|
+
readonly via: V;
|
|
63
|
+
})[];
|
|
64
|
+
readonly newlyConsumed: number;
|
|
65
|
+
readonly remaining: number;
|
|
66
|
+
};
|
package/dist/receive.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class ReceiveError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
key;
|
|
4
|
+
constructor(code, key) {
|
|
5
|
+
super(`${code}: ${String(key)}`);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.key = key;
|
|
8
|
+
this.name = 'ReceiveError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Storage rejects duplicate items/addresses; the caller rolls back the whole admission. */
|
|
12
|
+
export function receive(tx, input) {
|
|
13
|
+
tx.insertItem(input.item);
|
|
14
|
+
input.audience.forEach((address, order) => tx.insertAddress(input.item.key, address, order));
|
|
15
|
+
}
|
|
16
|
+
/** Queries filter before limiting. Exact selection ignores consumption, not addressing. */
|
|
17
|
+
export function selectReceivedHeads(tx, recipient, selection) {
|
|
18
|
+
if (selection.kind === 'page') {
|
|
19
|
+
if (typeof selection.limit !== 'number')
|
|
20
|
+
throw new TypeError('Receive limit must be a number.');
|
|
21
|
+
if (!Number.isSafeInteger(selection.limit) || selection.limit < 1)
|
|
22
|
+
throw new RangeError('Receive limit must be a positive safe integer.');
|
|
23
|
+
return tx.heads(recipient, selection.query, { unconsumed: true, limit: selection.limit });
|
|
24
|
+
}
|
|
25
|
+
const head = tx.addressed(recipient, selection.key);
|
|
26
|
+
if (head === undefined)
|
|
27
|
+
throw new ReceiveError('not_addressed', selection.key);
|
|
28
|
+
return [head];
|
|
29
|
+
}
|
|
30
|
+
/** Record captured coordinates without reselecting or checking their current eligibility. */
|
|
31
|
+
export function recordConsumed(tx, refs, at) {
|
|
32
|
+
if (typeof at !== 'number')
|
|
33
|
+
throw new TypeError('Receipt time must be a number.');
|
|
34
|
+
if (!Number.isFinite(at))
|
|
35
|
+
throw new RangeError('Receipt time must be finite.');
|
|
36
|
+
let count = 0;
|
|
37
|
+
for (const ref of refs)
|
|
38
|
+
if (tx.insertReceipt(ref, at))
|
|
39
|
+
count++;
|
|
40
|
+
return count;
|
|
41
|
+
}
|
|
42
|
+
/** Selection, hydration, receipts and remaining count must share the caller's transaction. */
|
|
43
|
+
export function consumeReceived(tx, recipient, selection, at) {
|
|
44
|
+
const heads = selectReceivedHeads(tx, recipient, selection);
|
|
45
|
+
const data = tx.load(heads.map(head => head.key));
|
|
46
|
+
const items = heads.map(head => {
|
|
47
|
+
if (!data.has(head.key))
|
|
48
|
+
throw new ReceiveError('missing_item', head.key);
|
|
49
|
+
return { key: head.key, data: data.get(head.key), via: head.via };
|
|
50
|
+
});
|
|
51
|
+
const newlyConsumed = recordConsumed(tx, heads.map(head => ({ recipient, item: head.key })), at);
|
|
52
|
+
const remaining = tx.count(recipient, selection.kind === 'page' ? selection.query : selection.remainingQuery, true);
|
|
53
|
+
return { items, newlyConsumed, remaining };
|
|
54
|
+
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type StoredAct, type WatchLease } from './model.js';
|
|
1
|
+
import { type WatchLease } from './model.js';
|
|
3
2
|
import type { ParticipantCounts, ParticipantFact, SquareRead } from './ports.js';
|
|
4
3
|
export declare const WATCH_HEARTBEAT_MS: number;
|
|
5
|
-
export declare const SLEEP_MS: number;
|
|
6
|
-
export declare const STALE_MS: number;
|
|
7
4
|
export declare const WATCH_STALE_MS: number;
|
|
8
5
|
export declare const THROTTLE_WINDOW_MS: number;
|
|
9
6
|
export declare const UNREAD_BLOCK_GRACE_MS: number;
|
|
10
|
-
export declare const HELD_WAIT_BUDGET_MS: number;
|
|
11
7
|
export declare const FACT_PAGE_SIZE = 100;
|
|
12
|
-
export declare function nowMs(): number;
|
|
13
|
-
export declare function rosterNames(facts: SquareRead): string[];
|
|
14
8
|
export declare function inSquareCount(facts: SquareRead): number;
|
|
15
|
-
export declare function actId(actOrIndex: StoredAct | number): ActivityId;
|
|
16
9
|
export declare function terminalStatusFor(current: ParticipantFact | undefined, hardCap: number | null, counts: ParticipantCounts): 'capped' | 'quorum' | undefined;
|
|
17
10
|
export declare function watchTerminalStatus(facts: SquareRead, name: string): 'capped' | 'quorum' | undefined;
|
|
18
11
|
/** A continuous prefix stops at the first unseen historical audience edge after join. */
|
package/dist/runtime.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { formatActivityId } from './square-core.js';
|
|
2
1
|
import { SquareError } from './model.js';
|
|
3
2
|
function parseIntegerEnv(name, fallback) {
|
|
4
3
|
const raw = process.env[name];
|
|
@@ -10,25 +9,11 @@ function parseIntegerEnv(name, fallback) {
|
|
|
10
9
|
return value;
|
|
11
10
|
}
|
|
12
11
|
export const WATCH_HEARTBEAT_MS = parseIntegerEnv('SQUARE_WATCH_HEARTBEAT_MS', 60000);
|
|
13
|
-
export const SLEEP_MS = parseIntegerEnv('SQUARE_WATCH_POLL_MS', 10000);
|
|
14
|
-
export const STALE_MS = parseIntegerEnv('SQUARE_WATCH_QUIET_MS', 300000);
|
|
15
12
|
export const WATCH_STALE_MS = parseIntegerEnv('SQUARE_WATCH_STALE_MS', 180000);
|
|
16
13
|
export const THROTTLE_WINDOW_MS = parseIntegerEnv('SQUARE_THROTTLE_WINDOW_MS', 60000);
|
|
17
14
|
export const UNREAD_BLOCK_GRACE_MS = parseIntegerEnv('SQUARE_UNREAD_BLOCK_GRACE_MS', 90000);
|
|
18
|
-
export const HELD_WAIT_BUDGET_MS = parseIntegerEnv('SQUARE_HELD_WAIT_BUDGET_MS', 10 * 60 * 1000);
|
|
19
15
|
export const FACT_PAGE_SIZE = 100;
|
|
20
|
-
export function nowMs() {
|
|
21
|
-
const override = process.env.SQUARE_NOW_MS;
|
|
22
|
-
if (override === undefined)
|
|
23
|
-
return Date.now();
|
|
24
|
-
const value = parseInt(override, 10);
|
|
25
|
-
if (!Number.isFinite(value))
|
|
26
|
-
throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
export function rosterNames(facts) { return facts.roster().map((participant) => participant.name); }
|
|
30
16
|
export function inSquareCount(facts) { return facts.participantCounts().joined; }
|
|
31
|
-
export function actId(actOrIndex) { return formatActivityId(typeof actOrIndex === 'number' ? actOrIndex : actOrIndex.index); }
|
|
32
17
|
export function terminalStatusFor(current, hardCap, counts) {
|
|
33
18
|
if (hardCap !== null && (current?.sayCount ?? 0) >= hardCap)
|
|
34
19
|
return 'capped';
|
|
@@ -47,8 +32,8 @@ export function readCursor(facts, name) {
|
|
|
47
32
|
const boundary = facts.participant(name)?.lastJoinIndex ?? -1;
|
|
48
33
|
const block = facts.directed(name, { afterIndex: boundary, unseenOnly: true }, { edge: 'oldest', limit: 1 }).items[0]?.index;
|
|
49
34
|
const last = facts.activityHeads({
|
|
50
|
-
afterIndex: boundary, ...(block === undefined ? {} : { beforeIndex: block }),
|
|
51
|
-
kinds: ['join', 'done', 'say', '
|
|
35
|
+
afterIndex: boundary, ...(block === undefined ? {} : { beforeIndex: block }),
|
|
36
|
+
kinds: ['join', 'done', 'say', 'listen', 'ignore', 'unlisten'],
|
|
52
37
|
}, { edge: 'newest', limit: 1 }).items[0]?.index;
|
|
53
38
|
return Math.max(boundary, last ?? boundary);
|
|
54
39
|
}
|
package/dist/seats.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Owner, type Seat } from './model.js';
|
|
2
2
|
import type { ArtifactWriteAuthority, SeatRef } from './ports.js';
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function requireOwner(owner: Owner | undefined): Owner;
|
|
4
4
|
export declare function requireJoinName(name: string | undefined): string;
|
|
5
5
|
export declare function asSeatRef(seat: Seat): SeatRef;
|
|
6
6
|
export declare function participantAuthority(seat: Seat): ArtifactWriteAuthority;
|
|
7
|
-
export declare function requireSeat(seat: Seat | undefined, body: Body | undefined, participant: string): Seat;
|
package/dist/seats.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { SquareError,
|
|
2
|
-
export function
|
|
3
|
-
if (
|
|
4
|
-
throw new SquareError('invalid_args', 'Participant writes require an explicit
|
|
5
|
-
|
|
6
|
-
return
|
|
1
|
+
import { SquareError, validateOwner, validateName } from './model.js';
|
|
2
|
+
export function requireOwner(owner) {
|
|
3
|
+
if (owner === undefined)
|
|
4
|
+
throw new SquareError('invalid_args', 'Participant writes require an explicit owner.');
|
|
5
|
+
validateOwner(owner);
|
|
6
|
+
return owner;
|
|
7
7
|
}
|
|
8
8
|
export function requireJoinName(name) {
|
|
9
9
|
if (name === undefined)
|
|
@@ -12,16 +12,8 @@ export function requireJoinName(name) {
|
|
|
12
12
|
return name;
|
|
13
13
|
}
|
|
14
14
|
export function asSeatRef(seat) {
|
|
15
|
-
|
|
16
|
-
return ref;
|
|
15
|
+
return { participant: seat.participant, owner: seat.owner, generation: seat.generation };
|
|
17
16
|
}
|
|
18
17
|
export function participantAuthority(seat) {
|
|
19
18
|
return { kind: 'participant', seat: asSeatRef(seat) };
|
|
20
19
|
}
|
|
21
|
-
export function requireSeat(seat, body, participant) {
|
|
22
|
-
if (seat === undefined || !sameName(seat.participant, participant))
|
|
23
|
-
throw new SquareError('already_joined', `${participant} needs a current participant handle; join first`);
|
|
24
|
-
if (!sameBody(seat.body, requireBody(body)))
|
|
25
|
-
throw new SquareError('already_joined', `${participant} is not held by this body`);
|
|
26
|
-
return seat;
|
|
27
|
-
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { type Owner } from './owner.js';
|
|
1
2
|
import type { AutomaticProvider } from './participant-identity.js';
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
import type { WakeAddress } from './model.js';
|
|
4
|
+
/** Host shutdown suspends execution, not membership. Discovery never grants custody. */
|
|
5
|
+
export declare function detachSessionSeats(provider: AutomaticProvider, sessionId: string, env?: NodeJS.ProcessEnv): Promise<void>;
|
|
6
|
+
/** Restore only detached, standing seats of this exact owner before starting a receiver. */
|
|
7
|
+
export declare function restoreSessionSeats(owner: Owner, env?: NodeJS.ProcessEnv, wakeAddress?: WakeAddress, signal?: AbortSignal): Promise<void>;
|
|
@@ -1,19 +1,47 @@
|
|
|
1
1
|
import { Square } from './square-api.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { harnessOwner } from './owner.js';
|
|
3
|
+
import { projectSessionCandidates, projectSessionSeats } from './inbox.js';
|
|
4
|
+
function ownerFor(provider, sessionId) {
|
|
5
|
+
return harnessOwner({ channel: provider === 'claude' ? 'claude-code' : provider, session: sessionId });
|
|
5
6
|
}
|
|
6
|
-
/**
|
|
7
|
-
export async function
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const square = await Square.at({ path: location, env, body, participant: seat.participant });
|
|
7
|
+
/** Host shutdown suspends execution, not membership. Discovery never grants custody. */
|
|
8
|
+
export async function detachSessionSeats(provider, sessionId, env = process.env) {
|
|
9
|
+
const owner = ownerFor(provider, sessionId);
|
|
10
|
+
for (const { location, seat } of await projectSessionSeats({ owner, env })) {
|
|
11
|
+
let square;
|
|
12
12
|
try {
|
|
13
|
-
await
|
|
13
|
+
square = await Square.at({ path: location, env });
|
|
14
|
+
await square.detach(seat.participant, { owner, generation: seat.generation });
|
|
14
15
|
}
|
|
16
|
+
catch { /* One unavailable or replaced seat must not block cleanup of the others. */ }
|
|
15
17
|
finally {
|
|
16
|
-
await square
|
|
18
|
+
await square?.close();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Restore only detached, standing seats of this exact owner before starting a receiver. */
|
|
23
|
+
export async function restoreSessionSeats(owner, env = process.env, wakeAddress, signal) {
|
|
24
|
+
const locations = new Set((await projectSessionCandidates({ owner, env })).map(candidate => candidate.location));
|
|
25
|
+
for (const location of locations) {
|
|
26
|
+
if (signal?.aborted)
|
|
27
|
+
return;
|
|
28
|
+
let square;
|
|
29
|
+
try {
|
|
30
|
+
square = await Square.at({ path: location, env }, { signal });
|
|
31
|
+
for (const seat of await square.seatsOf(owner)) {
|
|
32
|
+
if (signal?.aborted)
|
|
33
|
+
return;
|
|
34
|
+
if (seat.attachment !== 'detached')
|
|
35
|
+
continue;
|
|
36
|
+
try {
|
|
37
|
+
await square.reattach(seat.name, { owner, wakeAddress }, { signal });
|
|
38
|
+
}
|
|
39
|
+
catch { /* A replacement or one failed seat does not block its peers. */ }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch { /* An indexed location is only a candidate, not a promise of availability. */ }
|
|
43
|
+
finally {
|
|
44
|
+
await square?.close();
|
|
17
45
|
}
|
|
18
46
|
}
|
|
19
47
|
}
|