@astrosheep/square 0.3.58 → 0.3.62
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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/skills/square/SKILL.md +5 -7
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.d.ts +6 -2
- package/dist/activity.js +170 -133
- package/dist/artifact.d.ts +4 -0
- package/dist/artifact.js +43 -14
- package/dist/attention-presentation.d.ts +1 -0
- package/dist/attention-presentation.js +21 -19
- package/dist/automatic-session.js +16 -26
- package/dist/boundary-presentation.d.ts +1 -2
- package/dist/boundary-presentation.js +4 -19
- package/dist/catch-decisions.js +1 -1
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/context.js +17 -33
- package/dist/cli/harness-command.js +1 -1
- package/dist/cli/mcp-server-command.d.ts +2 -0
- package/dist/cli/mcp-server-command.js +13 -0
- package/dist/cli/observation-commands.js +132 -64
- package/dist/cli/program.js +36 -9
- package/dist/cli/registry.js +2 -0
- package/dist/cli/square-commands.d.ts +1 -1
- package/dist/cli/square-commands.js +103 -133
- package/dist/decisions.d.ts +4 -0
- package/dist/decisions.js +82 -34
- package/dist/delivery-operations.d.ts +1 -2
- package/dist/delivery-operations.js +1 -4
- package/dist/file-changes.d.ts +12 -0
- package/dist/file-changes.js +235 -0
- package/dist/help.js +4 -8
- package/dist/host-ledger-file-adapter.d.ts +15 -13
- package/dist/host-ledger-file-adapter.js +131 -79
- package/dist/host-ledger-root.d.ts +2 -0
- package/dist/host-ledger-root.js +9 -0
- package/dist/host-ledger.d.ts +4 -24
- package/dist/inbox.d.ts +10 -5
- package/dist/inbox.js +152 -67
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/list.js +12 -9
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +122 -0
- package/dist/mention-parse.d.ts +8 -0
- package/dist/mention-parse.js +107 -0
- package/dist/model.d.ts +5 -0
- package/dist/model.js +4 -1
- package/dist/notifications.d.ts +1 -0
- package/dist/notifications.js +25 -20
- package/dist/participant-host.js +2 -2
- package/dist/ports.d.ts +2 -9
- package/dist/presentation.d.ts +19 -1
- package/dist/presentation.js +111 -73
- package/dist/presented.js +2 -1
- package/dist/registry.d.ts +8 -4
- package/dist/registry.js +43 -76
- package/dist/routes.d.ts +19 -2
- package/dist/routes.js +48 -5
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.js +39 -0
- package/dist/square-actions.d.ts +26 -13
- package/dist/square-actions.js +203 -87
- package/dist/square-application.d.ts +58 -0
- package/dist/square-application.js +183 -0
- package/dist/square-facade.d.ts +24 -9
- package/dist/square-file-adapter.d.ts +5 -12
- package/dist/square-file-adapter.js +12 -64
- package/dist/square-projections.d.ts +3 -2
- package/dist/square-projections.js +12 -3
- package/dist/square-storage.js +27 -34
- package/dist/square-wiring.d.ts +12 -7
- package/dist/square-wiring.js +36 -32
- package/dist/state-cell.d.ts +1 -1
- package/dist/stream.js +31 -27
- package/dist/tty-style.d.ts +7 -0
- package/dist/tty-style.js +13 -0
- package/dist/views.d.ts +3 -3
- package/dist/views.js +10 -9
- package/dist/wake-attempts.js +2 -1
- package/dist/wake-evidence.js +2 -8
- package/dist/wakes.d.ts +1 -0
- package/dist/wakes.js +5 -5
- package/dist/watch.d.ts +6 -1
- package/dist/watch.js +43 -28
- package/extensions/square-pi.js +107 -109
- package/guides/application-boundary.md +32 -0
- package/guides/change-waits.md +28 -0
- package/guides/host-ledger.md +9 -0
- package/guides/mcp.md +9 -0
- package/guides/operation-cancellation.md +17 -0
- package/package.json +10 -4
- package/skills/square/SKILL.md +5 -7
|
@@ -63,9 +63,9 @@ The ownership boundary belongs here. Does this match your read?
|
|
|
63
63
|
EOF
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
**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. 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.
|
|
66
|
+
**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, and an unmatched `@name` is rejected rather than landing bare. 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.
|
|
67
67
|
|
|
68
|
-
Every human-CLI `express` must choose exactly one reach mode: one or more `--mention <name>` flags, `--no-mention`,
|
|
68
|
+
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.
|
|
69
69
|
|
|
70
70
|
**Discipline.** Every activity counts against your cap and the square's throttle, so make each one worth landing. Keep private progress and tool chatter out — express only when another participant needs the thought, question, or decision.
|
|
71
71
|
|
|
@@ -128,13 +128,11 @@ square --location <square> --as <name> resume
|
|
|
128
128
|
`done` is permanent, not the end of a conversation round. Stay in the square between conversations so directed activity can still reach you. Use `done` only after you no longer want to participate in or receive anything from this square — the whole square sees you go:
|
|
129
129
|
|
|
130
130
|
```bash
|
|
131
|
-
square --location <square> --as <name> done
|
|
132
|
-
*pushes the chair back*
|
|
133
|
-
|
|
134
|
-
Final state, decision, or handoff.
|
|
135
|
-
EOF
|
|
131
|
+
square --location <square> --as <name> done
|
|
136
132
|
```
|
|
137
133
|
|
|
134
|
+
`done` only leaves the square and takes no message. Send any activity with `express` before leaving.
|
|
135
|
+
|
|
138
136
|
## Environment
|
|
139
137
|
|
|
140
138
|
- `SQUARE_LOCATION` sets the square location.
|
package/dist/activity.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export interface ActivityOptions {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
env?: NodeJS.ProcessEnv;
|
|
2
4
|
force?: boolean;
|
|
3
|
-
forceCommand: string;
|
|
4
5
|
noWait?: boolean;
|
|
6
|
+
noMention?: boolean;
|
|
5
7
|
mentions?: readonly string[];
|
|
6
8
|
reach?: import('./model.js').Reach;
|
|
7
|
-
reply?:
|
|
9
|
+
reply?: string;
|
|
8
10
|
}
|
|
9
11
|
export declare function saveActivityDraft(squarePath: string, name: string, body: string): string;
|
|
12
|
+
/** Retry commands preserve the original reach, reply, wait and force choices. */
|
|
13
|
+
export declare function activityRetryCommand(squarePath: string, name: string, opts: ActivityOptions, force?: boolean): string;
|
|
10
14
|
export declare function cmdActivity(squarePath: string, name: string, activity: string, resolveBody: (arg: string) => string, opts: ActivityOptions): Promise<void>;
|
package/dist/activity.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
4
|
import { SquareError, isSquareError, validateName } from './model.js';
|
|
6
|
-
import { expressHintLine, renderActivityBlocked, renderActivityLimit, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, joinRecoveryCommand, withPathOutput, } from './presentation.js';
|
|
5
|
+
import { expressHintLine, formatRefusal, renderActivityBlocked, renderActivityLimit, renderActivityUncertain, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, joinRecoveryCommand, participantIdentity, participantsRecoveryCommand, participantCommandPrefix, commandPrefix, quoteShell, takeoverRecoveryLines, withPathOutput, } from './presentation.js';
|
|
7
6
|
import { nowMs, SLEEP_MS } from './runtime.js';
|
|
7
|
+
import { unreadActivitySummaries } from './decisions.js';
|
|
8
8
|
import { openSquare } from './square-file-adapter.js';
|
|
9
9
|
import { closeOpenSquare } from './open-square.js';
|
|
10
|
-
import {
|
|
10
|
+
import { createSquareApplication } from './square-application.js';
|
|
11
11
|
import { activityPresentation, resolveParticipant } from './views.js';
|
|
12
|
-
import { formatActivityId } from './square-core.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
13
|
+
import { formatDuration } from './time.js';
|
|
14
|
+
import { style } from './tty-style.js';
|
|
15
|
+
import { hostLedgerForEnv } from './registry.js';
|
|
15
16
|
import { createDefaultWakeTransport } from './notifications.js';
|
|
16
17
|
function draftDirFor(squarePath) {
|
|
17
18
|
return path.join(path.dirname(squarePath), 'drafts');
|
|
@@ -40,140 +41,176 @@ export function saveActivityDraft(squarePath, name, body) {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
}
|
|
44
|
+
/** Retry commands preserve the original reach, reply, wait and force choices. */
|
|
45
|
+
export function activityRetryCommand(squarePath, name, opts, force = opts.force ?? false) {
|
|
46
|
+
const reach = opts.reach === 'bell' ? ' --bell'
|
|
47
|
+
: opts.mentions?.length ? opts.mentions.map((target) => ` --mention ${quoteShell(target)}`).join('')
|
|
48
|
+
: opts.noMention === true ? ' --no-mention'
|
|
49
|
+
: '';
|
|
50
|
+
const replyIndex = opts.reply === undefined ? undefined : parseActivityId(opts.reply);
|
|
51
|
+
const reply = opts.reply === undefined ? '' : ` --reply ${replyIndex === undefined ? quoteShell(opts.reply) : formatActivityId(replyIndex)}`;
|
|
52
|
+
return `${participantCommandPrefix(squarePath, name)} express${force ? ' --force' : ''}${opts.noWait ? ' --no-wait' : ''}${reach}${reply} -`;
|
|
53
|
+
}
|
|
43
54
|
export async function cmdActivity(squarePath, name, activity, resolveBody, opts) {
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
process.stderr.write(err.message + '\n');
|
|
63
|
-
process.exit(err.code === 'not_found' ? 1 : 2);
|
|
55
|
+
const rawInput = String(resolveBody(activity));
|
|
56
|
+
const env = { ...(opts.env ?? process.env) };
|
|
57
|
+
const hostLedger = hostLedgerForEnv(env);
|
|
58
|
+
const retryCommand = activityRetryCommand(squarePath, name, opts);
|
|
59
|
+
const forceCommand = activityRetryCommand(squarePath, name, opts, true);
|
|
60
|
+
let knownName = name;
|
|
61
|
+
let callerMissing = false;
|
|
62
|
+
let expressAttempted = false;
|
|
63
|
+
let landedId;
|
|
64
|
+
let announcedWait;
|
|
65
|
+
async function presentation() {
|
|
66
|
+
const square = await openSquare(squarePath, { clock: nowMs, env, hostLedger });
|
|
67
|
+
try {
|
|
68
|
+
return await activityPresentation(square, knownName);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await closeOpenSquare(square);
|
|
64
72
|
}
|
|
65
|
-
throw err;
|
|
66
73
|
}
|
|
67
|
-
await closeOpenSquare(reader);
|
|
68
|
-
const body = rawInput.trim();
|
|
69
|
-
const force = opts.force ?? false;
|
|
70
|
-
const noWait = opts.noWait ?? false;
|
|
71
|
-
const reach = opts.reach === 'bell' ? 'bell' : undefined;
|
|
72
|
-
let announcedWait;
|
|
73
|
-
const ledgerRoot = process.env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(process.env.SQUARE_REGISTRY);
|
|
74
|
-
const hostLedger = createHostLedgerPort({ userPath: process.env.SQUARE_HOST_LEDGER_USER ?? ledgerRoot, localPath: process.env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot });
|
|
75
|
-
const square = await Square.at({ path: squarePath, clock: nowMs, hostLedger, wakeTransport: await createDefaultWakeTransport(hostLedger, nowMs) });
|
|
76
74
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
75
|
+
validateName(name);
|
|
76
|
+
for (const target of opts.mentions ?? [])
|
|
77
|
+
validateName(target);
|
|
78
|
+
if (opts.reply !== undefined && parseActivityId(opts.reply) === undefined) {
|
|
79
|
+
throw new SquareError('invalid_args', 'Invalid --reply: expected an activity id like act/12');
|
|
80
|
+
}
|
|
81
|
+
const reader = await openSquare(squarePath, { clock: nowMs, env, hostLedger });
|
|
82
|
+
try {
|
|
83
|
+
knownName = (await resolveParticipant(reader, name)).name;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// The caller name was validated above; invalid_args here means it is absent
|
|
87
|
+
// from the roster. Keep the join recovery distinct from recipient validation.
|
|
88
|
+
callerMissing = isSquareError(error) && error.code === 'invalid_args';
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await closeOpenSquare(reader);
|
|
93
|
+
}
|
|
94
|
+
const application = createSquareApplication({ cwd: opts.cwd ?? process.cwd(), env, squarePath, clock: nowMs, hostLedger, wakeTransport: await createDefaultWakeTransport(hostLedger, nowMs), participant: name });
|
|
95
|
+
// The caller is roster-known at this point, so a landed join act is always a
|
|
96
|
+
// re-entry after done — never a first arrival. Announce it; a silent return
|
|
97
|
+
// would make `done` a fake exit.
|
|
98
|
+
const entry = await application.join();
|
|
99
|
+
const reentered = entry.kind === 'joined' && entry.activity !== null;
|
|
100
|
+
const before = await presentation();
|
|
101
|
+
const pendingPublic = before.pendingPublic;
|
|
102
|
+
const pendingRoomChanges = before.pendingRoomChanges;
|
|
103
|
+
expressAttempted = true;
|
|
104
|
+
const landed = await application.express(rawInput.replace(/\r\n/g, '\n').trim(), {
|
|
105
|
+
force: opts.force ?? false,
|
|
106
|
+
noWait: opts.noWait ?? false,
|
|
107
|
+
...(opts.mentions === undefined ? {} : { mentions: opts.mentions }),
|
|
108
|
+
...(opts.noMention === undefined ? {} : { noMention: opts.noMention }),
|
|
109
|
+
...(opts.reach === undefined ? {} : { reach: opts.reach }),
|
|
110
|
+
// The action boundary validates the raw id before attempting a commit.
|
|
111
|
+
...(opts.reply === undefined ? {} : { reply: opts.reply }),
|
|
112
|
+
}, {
|
|
113
|
+
onProgress: (progress) => {
|
|
114
|
+
if (announcedWait === progress.reason)
|
|
115
|
+
return;
|
|
116
|
+
const delayMs = progress.delayMs ?? SLEEP_MS;
|
|
117
|
+
process.stdout.write(renderExpressWaiting({
|
|
118
|
+
reason: progress.reason,
|
|
119
|
+
...(progress.reason === 'throttled' ? { delayMs } : {}),
|
|
120
|
+
...(progress.holder === undefined ? {} : { holder: progress.holder }),
|
|
121
|
+
...(progress.holdReason === undefined ? {} : { holdReason: progress.holdReason }),
|
|
122
|
+
}) + '\n');
|
|
123
|
+
announcedWait = progress.reason;
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
landedId = landed.activity.id;
|
|
127
|
+
const fresh = await presentation();
|
|
128
|
+
const headerCount = fresh.participantCount;
|
|
129
|
+
const held = fresh.held;
|
|
130
|
+
const ownActCount = fresh.ownActivityCount;
|
|
131
|
+
const hasPending = pendingPublic.length > 0 || pendingRoomChanges.length > 0;
|
|
132
|
+
const pending = hasPending ? `\n\n${renderPendingFeed([...fresh.activities], [...pendingPublic], [...pendingRoomChanges], knownName, fresh.state)}` : '';
|
|
133
|
+
const hint = expressHintLine(ownActCount);
|
|
134
|
+
const reachEcho = landed.activity.reach === 'bell'
|
|
135
|
+
? ' · to everyone (bell)'
|
|
136
|
+
: landed.activity.mentions.length > 0
|
|
137
|
+
? ` · to ${landed.activity.mentions.map((target) => participantIdentity(target)).join(', ')}`
|
|
138
|
+
: '';
|
|
139
|
+
const confirmation = `● your activity lands${style('dim', ` — #${ownActCount} · ${landed.activity.id}${reachEcho}`)}`;
|
|
140
|
+
const withHint = hint ? `${confirmation}\n${style('dim', hint)}` : confirmation;
|
|
141
|
+
const reentry = reentered ? '● you stepped back into the square\n' : '';
|
|
142
|
+
process.stdout.write(withPathOutput(squarePath, reentry + withHint + pending, { participantCount: headerCount, held }));
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// Save before any recovery read: even a broken artifact must not eat the body.
|
|
146
|
+
const draftPath = saveActivityDraft(squarePath, name, rawInput);
|
|
147
|
+
const confirmedUnsent = landedId === undefined && (!expressAttempted || (isSquareError(error) && error.facts?.activityUnsent === true));
|
|
148
|
+
const fresh = await presentation().catch(() => undefined);
|
|
149
|
+
const output = { squarePath, name: knownName, draftPath, participantCount: fresh?.participantCount, held: fresh?.held };
|
|
150
|
+
if (!confirmedUnsent) {
|
|
151
|
+
process.stderr.write(renderActivityUncertain({ ...output, landedId, detail: error instanceof Error ? error.message : String(error) }));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (isSquareError(error) && fresh !== undefined) {
|
|
156
|
+
if (error.code === 'behind') {
|
|
157
|
+
process.stdout.write(renderActivityBlocked({
|
|
158
|
+
...output,
|
|
159
|
+
retryCommand,
|
|
160
|
+
forceCommand,
|
|
161
|
+
activitySummaries: unreadActivitySummaries(fresh.state, knownName, nowMs()),
|
|
162
|
+
unreadRoomChanges: [],
|
|
163
|
+
}));
|
|
164
|
+
process.exit(1);
|
|
101
165
|
return;
|
|
102
166
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
held,
|
|
134
|
-
}));
|
|
135
|
-
process.exit(1);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if (error.code === 'throttled') {
|
|
139
|
-
const delayMs = error.facts?.retryAfterMs ?? SLEEP_MS;
|
|
140
|
-
if (noWait) {
|
|
141
|
-
const draftPath = saveActivityDraft(squarePath, name, rawInput);
|
|
142
|
-
process.stdout.write(renderExpressNoWait({ squarePath, name: knownName, reason: 'throttled', delayMs, draftPath, participantCount: headerCount, held, forceCommand: opts.forceCommand }));
|
|
143
|
-
process.exit(1);
|
|
144
|
-
}
|
|
145
|
-
if (announcedWait !== 'throttled') {
|
|
146
|
-
process.stdout.write(renderExpressWaiting({ reason: 'throttled', delayMs }) + '\n');
|
|
147
|
-
announcedWait = 'throttled';
|
|
148
|
-
}
|
|
149
|
-
await sleep(delayMs);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (error.code === 'held') {
|
|
153
|
-
const holdReason = fresh.holdReason;
|
|
154
|
-
if (noWait) {
|
|
155
|
-
const draftPath = saveActivityDraft(squarePath, name, rawInput);
|
|
156
|
-
process.stdout.write(renderExpressNoWait({ squarePath, name: knownName, reason: 'held', holdReason, draftPath, participantCount: headerCount, held, forceCommand: opts.forceCommand }));
|
|
157
|
-
process.exit(1);
|
|
158
|
-
}
|
|
159
|
-
if (announcedWait !== 'held') {
|
|
160
|
-
process.stdout.write(renderExpressWaiting({ reason: 'held' }) + '\n');
|
|
161
|
-
announcedWait = 'held';
|
|
162
|
-
}
|
|
163
|
-
await sleep(SLEEP_MS);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (error.code === 'bell_quota') {
|
|
167
|
-
const nextAt = nowMs() + (error.facts?.retryAfterMs ?? 1);
|
|
168
|
-
process.stdout.write(withPathOutput(squarePath, [`✕ the bell stays quiet for now`, ` · you can ring it again at ${formatTimestamp(nextAt)}`].join('\n'), { participantCount: headerCount, held }));
|
|
169
|
-
process.exit(1);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
throw error;
|
|
167
|
+
if (error.code === 'capped') {
|
|
168
|
+
process.stdout.write(renderActivityLimit({
|
|
169
|
+
...output,
|
|
170
|
+
count: fresh.ownActivityCount,
|
|
171
|
+
...(fresh.hardCap === null ? {} : { hardCap: fresh.hardCap }),
|
|
172
|
+
}));
|
|
173
|
+
process.exit(1);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (error.code === 'throttled' || error.code === 'held') {
|
|
177
|
+
process.stdout.write(renderExpressNoWait({
|
|
178
|
+
...output,
|
|
179
|
+
reason: error.code,
|
|
180
|
+
delayMs: error.facts?.retryAfterMs ?? SLEEP_MS,
|
|
181
|
+
holdReason: fresh.holdReason,
|
|
182
|
+
retryCommand,
|
|
183
|
+
}));
|
|
184
|
+
process.exit(1);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (error.code === 'bell_quota') {
|
|
188
|
+
process.stdout.write(withPathOutput(squarePath, [
|
|
189
|
+
'✕ the bell stays quiet for now',
|
|
190
|
+
` · you can ring it again in ${formatDuration(error.facts?.retryAfterMs ?? 1)}`,
|
|
191
|
+
`· draft kept: ${draftPath}`,
|
|
192
|
+
` · retry when the bell is ready:`,
|
|
193
|
+
`${retryCommand} < ${quoteShell(draftPath)}`,
|
|
194
|
+
].join('\n'), output));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
return;
|
|
173
197
|
}
|
|
174
198
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
199
|
+
const lines = [error instanceof Error ? error.message : String(error), `· draft kept: ${draftPath}`];
|
|
200
|
+
if (isSquareError(error)) {
|
|
201
|
+
if (callerMissing || error.code === 'unknown_participant' || error.code === 'not_joined')
|
|
202
|
+
lines.push(joinRecoveryCommand(squarePath, name));
|
|
203
|
+
if (error.code === 'already_joined')
|
|
204
|
+
lines.push(...takeoverRecoveryLines(squarePath, name));
|
|
205
|
+
if (error.code === 'invalid_args' || error.code === 'invalid_name') {
|
|
206
|
+
if (opts.mentions?.length)
|
|
207
|
+
lines.push(participantsRecoveryCommand(squarePath));
|
|
208
|
+
if (opts.reply !== undefined)
|
|
209
|
+
lines.push(`${commandPrefix(squarePath)} history --limit 10`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
lines.push(' · correct the problem above before retrying; keep your intended recipients and reply:', `${retryCommand} < ${quoteShell(draftPath)}`);
|
|
213
|
+
process.stderr.write(formatRefusal(squarePath, lines, output));
|
|
214
|
+
process.exit(2);
|
|
178
215
|
}
|
|
179
216
|
}
|
package/dist/artifact.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ export declare function createSquareFile(squarePath: string, state: SquareState)
|
|
|
20
20
|
* conflicts. The URI remains create-disabled throughout the retry loop.
|
|
21
21
|
*/
|
|
22
22
|
export declare function readSquareSnapshot(squarePath: string, signal?: AbortSignal): Promise<SquareSnapshot>;
|
|
23
|
+
/** Check the artifact revision without loading or parsing the historical state. */
|
|
24
|
+
export declare function readSquareRevision(squarePath: string, signal?: AbortSignal): Promise<number>;
|
|
25
|
+
/** One reference-counted revision detector per canonical artifact in this process. */
|
|
26
|
+
export declare function observeSquareRevision(squarePath: string): Promise<import('./file-changes.js').VersionObserver<number>>;
|
|
23
27
|
export type SquareTransition<R> = (state: SquareState, revision: number) => {
|
|
24
28
|
state?: SquareState;
|
|
25
29
|
result: R;
|
package/dist/artifact.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
7
7
|
import { isDeepStrictEqual } from 'node:util';
|
|
8
|
+
import { observeFileVersion } from './file-changes.js';
|
|
8
9
|
import { InternalSquareError, SquareError } from './model.js';
|
|
9
10
|
import { validateSquareState } from './square-state.js';
|
|
10
11
|
const APPLICATION_ID = 0x53515245;
|
|
@@ -86,19 +87,26 @@ function isOwnedSnapshotSchema(database) {
|
|
|
86
87
|
&& column.pk === expected.pk && column.hidden === expected.hidden;
|
|
87
88
|
});
|
|
88
89
|
}
|
|
90
|
+
function validateDatabaseHeader(database) {
|
|
91
|
+
const identity = database.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
|
|
92
|
+
if (identity.application_id !== APPLICATION_ID)
|
|
93
|
+
throw invalidArtifact('unrelated SQLite database.');
|
|
94
|
+
if (identity.user_version !== USER_VERSION)
|
|
95
|
+
throw invalidArtifact('unsupported SQLite format version.');
|
|
96
|
+
if (!isOwnedSnapshotSchema(database))
|
|
97
|
+
throw invalidArtifact('unsupported snapshot schema.');
|
|
98
|
+
}
|
|
99
|
+
function validateSnapshotRevision(rows) {
|
|
100
|
+
if (rows.length !== 1 || rows[0].id !== 1 || !Number.isSafeInteger(rows[0].revision) || rows[0].revision < 0)
|
|
101
|
+
throw invalidArtifact('snapshot row is malformed.');
|
|
102
|
+
return rows[0].revision;
|
|
103
|
+
}
|
|
89
104
|
function validateDatabase(database) {
|
|
90
105
|
try {
|
|
91
|
-
|
|
92
|
-
if (identity.application_id !== APPLICATION_ID)
|
|
93
|
-
throw invalidArtifact('unrelated SQLite database.');
|
|
94
|
-
if (identity.user_version !== USER_VERSION)
|
|
95
|
-
throw invalidArtifact('unsupported SQLite format version.');
|
|
96
|
-
if (!isOwnedSnapshotSchema(database))
|
|
97
|
-
throw invalidArtifact('unsupported snapshot schema.');
|
|
106
|
+
validateDatabaseHeader(database);
|
|
98
107
|
const rows = database.prepare(`SELECT id, revision, state FROM ${SNAPSHOT_TABLE}`).all();
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return { state: parseState(rows[0].state), revision: rows[0].revision };
|
|
108
|
+
const revision = validateSnapshotRevision(rows);
|
|
109
|
+
return { state: parseState(rows[0].state), revision };
|
|
102
110
|
}
|
|
103
111
|
catch (error) {
|
|
104
112
|
if (error instanceof SquareError)
|
|
@@ -112,12 +120,12 @@ function configureDatabase(database) {
|
|
|
112
120
|
database.exec('PRAGMA busy_timeout = 0; PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL;');
|
|
113
121
|
}
|
|
114
122
|
/** Opens an existing artifact without permitting SQLite to create a missing path. */
|
|
115
|
-
function
|
|
123
|
+
function readExistingArtifact(squarePath, read) {
|
|
116
124
|
requireSquareExtension(squarePath);
|
|
117
125
|
let database;
|
|
118
126
|
try {
|
|
119
127
|
database = openExistingDatabase(squarePath);
|
|
120
|
-
return
|
|
128
|
+
return read(database);
|
|
121
129
|
}
|
|
122
130
|
catch (error) {
|
|
123
131
|
if (error instanceof SquareError)
|
|
@@ -132,6 +140,16 @@ function readSquareSnapshotOnce(squarePath) {
|
|
|
132
140
|
closeQuietly(database);
|
|
133
141
|
}
|
|
134
142
|
}
|
|
143
|
+
function readSquareSnapshotOnce(squarePath) {
|
|
144
|
+
return readExistingArtifact(squarePath, validateDatabase);
|
|
145
|
+
}
|
|
146
|
+
function readSquareRevisionOnce(squarePath) {
|
|
147
|
+
return readExistingArtifact(squarePath, (database) => {
|
|
148
|
+
validateDatabaseHeader(database);
|
|
149
|
+
const rows = database.prepare(`SELECT id, revision FROM ${SNAPSHOT_TABLE}`).all();
|
|
150
|
+
return validateSnapshotRevision(rows);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
135
153
|
async function createTemporaryArtifact(squarePath) {
|
|
136
154
|
const directory = path.dirname(squarePath);
|
|
137
155
|
await fs.promises.mkdir(directory, { recursive: true });
|
|
@@ -236,12 +254,23 @@ async function pauseForBusy(signal) {
|
|
|
236
254
|
* Reads a complete validated snapshot, yielding between transient SQLite lock
|
|
237
255
|
* conflicts. The URI remains create-disabled throughout the retry loop.
|
|
238
256
|
*/
|
|
239
|
-
export
|
|
257
|
+
export function readSquareSnapshot(squarePath, signal) {
|
|
258
|
+
return readWithBusyRetry(() => readSquareSnapshotOnce(squarePath), signal);
|
|
259
|
+
}
|
|
260
|
+
/** Check the artifact revision without loading or parsing the historical state. */
|
|
261
|
+
export function readSquareRevision(squarePath, signal) {
|
|
262
|
+
return readWithBusyRetry(() => readSquareRevisionOnce(squarePath), signal);
|
|
263
|
+
}
|
|
264
|
+
/** One reference-counted revision detector per canonical artifact in this process. */
|
|
265
|
+
export function observeSquareRevision(squarePath) {
|
|
266
|
+
return observeFileVersion(squarePath, 'square-revision', readSquareRevision);
|
|
267
|
+
}
|
|
268
|
+
async function readWithBusyRetry(read, signal) {
|
|
240
269
|
while (true) {
|
|
241
270
|
if (signal?.aborted)
|
|
242
271
|
throw signal.reason ?? closedError();
|
|
243
272
|
try {
|
|
244
|
-
return
|
|
273
|
+
return read();
|
|
245
274
|
}
|
|
246
275
|
catch (error) {
|
|
247
276
|
if (!isBusy(error))
|
|
@@ -11,4 +11,5 @@ export interface AttentionPreview {
|
|
|
11
11
|
export declare function previewAttentionBody(body: string): string;
|
|
12
12
|
export declare function attentionBodyIsClipped(body: string): boolean;
|
|
13
13
|
export declare function displayAttentionPath(squarePath: string): string;
|
|
14
|
+
export declare function renderAttentionDescription({ actor, recipient, route }: Pick<AttentionPreview, 'actor' | 'recipient' | 'route'>): string;
|
|
14
15
|
export declare function renderAttentionPreview(attention: AttentionPreview): string;
|
|
@@ -16,30 +16,32 @@ export function displayAttentionPath(squarePath) {
|
|
|
16
16
|
? `~${squarePath.slice(homedir().length)}`
|
|
17
17
|
: squarePath;
|
|
18
18
|
}
|
|
19
|
+
export function renderAttentionDescription({ actor, recipient, route }) {
|
|
20
|
+
switch (route) {
|
|
21
|
+
case 'mention': return `${actor} addressed you (${recipient})`;
|
|
22
|
+
case 'attention': return `${actor} spoke · you’re listening to ${actor}`;
|
|
23
|
+
case 'bell': return `${actor} rang the bell · everyone’s attention`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
19
26
|
export function renderAttentionPreview(attention) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
from: attention.actor,
|
|
25
|
-
to: attention.recipient,
|
|
26
|
-
kind: attentionKind,
|
|
27
|
-
};
|
|
28
|
-
const entries = Object.entries(attributes);
|
|
29
|
-
const [lastKey, lastValue] = entries[entries.length - 1];
|
|
27
|
+
// The fence is what keeps the body from being re-rendered by Markdown. It grows past any
|
|
28
|
+
// backtick run in the body so a body containing a fence cannot break out of this one.
|
|
29
|
+
// Everything outside the fence is the system's voice, which participant bodies cannot fake.
|
|
30
|
+
const fence = '`'.repeat(Math.max(3, longestBacktickRun(attention.body) + 1));
|
|
30
31
|
return [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
`${fence}square-activity`,
|
|
33
|
+
`· ${displayAttentionPath(attention.squarePath)} · ${formatActivityId(attention.actIndex)}`,
|
|
34
|
+
`● ${renderAttentionDescription(attention)}`,
|
|
35
|
+
'',
|
|
35
36
|
previewAttentionBody(attention.body),
|
|
36
|
-
|
|
37
|
+
fence,
|
|
38
|
+
// A clipped body ends with the full command to read it all: a labeled caption, then the
|
|
39
|
+
// command bare on its own line so it can be copied and run as it is.
|
|
37
40
|
...(attentionBodyIsClipped(attention.body)
|
|
38
|
-
? [
|
|
41
|
+
? ['· clipped — read it all:', `${participantCommandPrefix(attention.squarePath, attention.recipient)} catch --id ${formatActivityId(attention.actIndex)}`]
|
|
39
42
|
: []),
|
|
40
43
|
].join('\n');
|
|
41
44
|
}
|
|
42
|
-
function
|
|
43
|
-
return value.
|
|
44
|
-
.replace(/\r/g, ' ').replace(/\n/g, ' ').replace(/\t/g, '	');
|
|
45
|
+
function longestBacktickRun(value) {
|
|
46
|
+
return value.match(/`+/g)?.reduce((longest, run) => Math.max(longest, run.length), 0) ?? 0;
|
|
45
47
|
}
|