@astrosheep/square 0.3.12 → 0.3.13
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/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/hooks/hooks.json +9 -0
- package/dist/activity-feed.d.ts +14 -0
- package/dist/activity-feed.js +5 -5
- package/dist/activity.d.ts +9 -0
- package/dist/activity.js +111 -90
- package/dist/artifact.d.ts +20 -0
- package/dist/artifact.js +10 -29
- package/dist/automatic-session.d.ts +5 -0
- package/dist/automatic-session.js +86 -0
- package/dist/boundary-presentation.d.ts +5 -0
- package/dist/boundary-presentation.js +5 -4
- package/dist/claude-hook.d.ts +9 -0
- package/dist/claude-hook.js +35 -3
- package/dist/cli/context.d.ts +35 -0
- package/dist/cli/context.js +25 -30
- package/dist/cli/harness-command.d.ts +28 -0
- package/dist/cli/harness-command.js +29 -14
- package/dist/cli/maintenance-commands.d.ts +7 -0
- package/dist/cli/maintenance-commands.js +3 -3
- package/dist/cli/meta-commands.d.ts +5 -0
- package/dist/cli/observation-commands.d.ts +20 -0
- package/dist/cli/observation-commands.js +155 -128
- package/dist/cli/program.d.ts +2 -0
- package/dist/cli/program.js +2 -12
- package/dist/cli/registry.d.ts +9 -0
- package/dist/cli/registry.js +1 -2
- package/dist/cli/square-commands.d.ts +34 -0
- package/dist/cli/square-commands.js +81 -74
- package/dist/cmd/notify-once.d.ts +2 -0
- package/dist/codex-hook.d.ts +10 -0
- package/dist/codex-hook.js +35 -3
- package/dist/decisions.d.ts +92 -0
- package/dist/decisions.js +83 -77
- package/dist/delivery-health.d.ts +21 -0
- package/dist/delivery-health.js +40 -34
- package/dist/delivery.d.ts +81 -0
- package/dist/delivery.js +60 -22
- package/dist/file-lock.d.ts +6 -0
- package/dist/harness-claude.d.ts +19 -0
- package/dist/harness-claude.js +8 -3
- package/dist/harness-codex.d.ts +29 -0
- package/dist/harness-codex.js +2 -2
- package/dist/harness-links.d.ts +17 -0
- package/dist/harness-links.js +2 -2
- package/dist/harness-pi.d.ts +11 -0
- package/dist/harness-pi.js +2 -2
- package/dist/harness-stage.d.ts +6 -0
- package/dist/harness.d.ts +20 -0
- package/dist/harness.js +5 -3
- package/dist/help.d.ts +6 -0
- package/dist/help.js +6 -5
- package/dist/identity.d.ts +13 -0
- package/dist/inbox.d.ts +2 -0
- package/dist/inbox.js +15 -19
- package/dist/index.d.ts +13 -0
- package/dist/index.js +31 -69
- package/dist/landing.d.ts +10 -0
- package/dist/landing.js +95 -0
- package/dist/list.d.ts +1 -0
- package/dist/list.js +40 -18
- package/dist/model.d.ts +147 -0
- package/dist/model.js +12 -2
- package/dist/notifications.d.ts +30 -0
- package/dist/notifications.js +75 -91
- package/dist/open-square.d.ts +11 -0
- package/dist/open-square.js +4 -0
- package/dist/participant-identity.d.ts +1 -0
- package/dist/participant-identity.js +3 -0
- package/dist/paseo-connection.d.ts +15 -0
- package/dist/paseo-delivery.d.ts +14 -0
- package/dist/paseo-state.d.ts +11 -0
- package/dist/paseo-timeline.d.ts +16 -0
- package/dist/presence.d.ts +3 -0
- package/dist/presence.js +42 -0
- package/dist/presentation.d.ts +91 -0
- package/dist/presentation.js +33 -29
- package/dist/presented.d.ts +11 -0
- package/dist/registry.d.ts +58 -0
- package/dist/registry.js +36 -3
- package/dist/routes.d.ts +42 -0
- package/dist/runtime.d.ts +45 -0
- package/dist/runtime.js +42 -34
- package/dist/search.d.ts +11 -0
- package/dist/square-core.d.ts +111 -0
- package/dist/square-core.js +7 -0
- package/dist/square-facade.d.ts +86 -0
- package/dist/square-facade.js +1 -0
- package/dist/square-file-adapter.d.ts +18 -0
- package/dist/square-file-adapter.js +70 -0
- package/dist/square-storage.d.ts +20 -0
- package/dist/square-storage.js +171 -0
- package/dist/square-wiring.d.ts +19 -0
- package/dist/square-wiring.js +70 -0
- package/dist/square.d.ts +2 -0
- package/dist/state-cell.d.ts +14 -0
- package/dist/state-cell.js +1 -0
- package/dist/stream.d.ts +3 -0
- package/dist/stream.js +19 -23
- package/dist/time.d.ts +6 -0
- package/dist/views.d.ts +95 -0
- package/dist/views.js +81 -0
- package/dist/wake-attempts.d.ts +39 -0
- package/dist/wake-evidence.d.ts +12 -0
- package/dist/wake-evidence.js +7 -10
- package/dist/wake-port.d.ts +20 -0
- package/dist/wake-sink.d.ts +12 -0
- package/dist/wakes.d.ts +40 -0
- package/dist/wakes.js +62 -0
- package/dist/watch.d.ts +2 -0
- package/dist/watch.js +89 -210
- package/extensions/square-opencode.js +27 -1
- package/extensions/square-pi.js +13 -0
- package/package.json +5 -2
- package/skills/brainstorm/SKILL.md +26 -26
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +47 -29
- package/skills/square/hooks/hooks.json +9 -0
- package/skills/square-feedback/SKILL.md +2 -2
- package/dist/compact.js +0 -26
- package/dist/square-application.js +0 -193
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { cmdActivity } from '../activity.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
2
|
+
import { formatActivityId, formatHardCap, parseActivityId, } from '../model.js';
|
|
3
|
+
import { participantCommandPrefix, participantIdentity, renderEventCli, renderAmbientEvent, withPathOutput, } from '../presentation.js';
|
|
4
|
+
import { hasAutomaticDeliveryIdentity, localParticipantOwner, recordLocalDone, recordLocalJoin, } from '../registry.js';
|
|
5
|
+
import { sweepPendingNotifications, wakeNotifierForSquare } from '../notifications.js';
|
|
6
|
+
import { nowMs } from '../runtime.js';
|
|
7
|
+
import { createSquare, openSquare } from '../square-file-adapter.js';
|
|
8
|
+
import { closeOpenSquare } from '../open-square.js';
|
|
9
|
+
import { Square } from '../square-wiring.js';
|
|
10
|
+
import { entryPresentation, eventPresentation } from '../views.js';
|
|
11
11
|
import { fail, parseHardCap, parsePositiveInteger, readPipedBodyFallback, readStdinSync, requireParticipant, requireValue, resolveBody, usage, } from './context.js';
|
|
12
12
|
function parseBuild(argv) {
|
|
13
13
|
const options = { force: false, hardCap: null };
|
|
@@ -79,57 +79,55 @@ function parseJoin(argv, context) {
|
|
|
79
79
|
export const joinCommand = {
|
|
80
80
|
parse: parseJoin,
|
|
81
81
|
async execute(intent, context) {
|
|
82
|
-
|
|
82
|
+
const beforeSquare = await openSquare(context.squarePath, { clock: nowMs });
|
|
83
|
+
const before = await entryPresentation(beforeSquare, intent.name, intent.lastN);
|
|
84
|
+
await closeOpenSquare(beforeSquare);
|
|
85
|
+
const square = await Square.at({ path: context.squarePath, clock: nowMs, notifier: wakeNotifierForSquare(context.squarePath) });
|
|
83
86
|
try {
|
|
84
|
-
const
|
|
85
|
-
const joinedName =
|
|
86
|
-
const isRejoin =
|
|
87
|
-
const
|
|
88
|
-
|
|
87
|
+
const participant = await square.join(intent.name);
|
|
88
|
+
const joinedName = participant.name;
|
|
89
|
+
const isRejoin = before.joined;
|
|
90
|
+
const reconnect = isRejoin
|
|
91
|
+
&& localParticipantOwner(context.squarePath, joinedName) !== undefined;
|
|
92
|
+
if (isRejoin && !intent.kick && !reconnect) {
|
|
93
|
+
fail([
|
|
94
|
+
`✕ ${participantIdentity(joinedName)} shoos you out of the square`,
|
|
95
|
+
` · a same-named participant stands here — the name is taken`,
|
|
96
|
+
` · --kick banishes her and the name becomes yours`,
|
|
97
|
+
`» ${participantCommandPrefix(context.squarePath, joinedName)} join --kick`,
|
|
98
|
+
].join('\n'));
|
|
99
|
+
}
|
|
100
|
+
const afterSquare = await openSquare(context.squarePath, { clock: nowMs });
|
|
101
|
+
const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
|
|
102
|
+
await closeOpenSquare(afterSquare);
|
|
89
103
|
recordLocalJoin(joinedName, context.squarePath);
|
|
90
104
|
await sweepPendingNotifications(context.squarePath);
|
|
91
|
-
const activities =
|
|
92
|
-
|
|
105
|
+
const activities = after.recentActivities.map((event) => renderAmbientEvent(event, joinedName, {
|
|
106
|
+
now: nowMs(),
|
|
107
|
+
preview: intent.lastN === null ? undefined : 200,
|
|
108
|
+
actNumber: event.kind === 'say' ? after.sayNumbers[event.index] : undefined,
|
|
109
|
+
})).filter(Boolean).join('\n\n');
|
|
110
|
+
const contextText = after.joinContext;
|
|
93
111
|
const fallback = hasAutomaticDeliveryIdentity()
|
|
94
112
|
? []
|
|
95
113
|
: ['', `» ${participantCommandPrefix(context.squarePath, joinedName)} catch --idle 30m`, ' no session delivery detected — keep this catch open for new activity'];
|
|
96
|
-
const scene = after.
|
|
114
|
+
const scene = after.scene;
|
|
115
|
+
const entryLine = !isRejoin
|
|
116
|
+
? '● You stepped into the square'
|
|
117
|
+
: reconnect && !intent.kick
|
|
118
|
+
? '● you are already in the square'
|
|
119
|
+
: `✓ you banished the original ${participantIdentity(joinedName)} — the name is yours`;
|
|
97
120
|
const output = [
|
|
98
|
-
|
|
99
|
-
...(
|
|
121
|
+
entryLine,
|
|
122
|
+
...(reconnect || scene === '' ? [] : ['', scene]),
|
|
100
123
|
...(isRejoin || contextText === '' ? [] : ['', 'context', contextText]),
|
|
101
|
-
...(activities === '' ? [] : ['', 'recent activity', activities]),
|
|
124
|
+
...(isRejoin || activities === '' ? [] : ['', 'recent activity', activities]),
|
|
102
125
|
...fallback,
|
|
103
126
|
].join('\n');
|
|
104
|
-
return withPathOutput(context.squarePath, output, { participantCount:
|
|
127
|
+
return withPathOutput(context.squarePath, output, { participantCount: after.participantCount });
|
|
105
128
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
throw error;
|
|
109
|
-
const doc = loadSquare(context.squarePath);
|
|
110
|
-
const joinedName = resolveRosterName(doc, intent.name);
|
|
111
|
-
if (joinedName === undefined || !isCurrentlyJoined(doc.acts, joinedName))
|
|
112
|
-
throw error;
|
|
113
|
-
const reconnect = localParticipantOwner(context.squarePath, joinedName) !== undefined;
|
|
114
|
-
if (!intent.kick && !reconnect) {
|
|
115
|
-
fail([
|
|
116
|
-
`✕ ${joinedName} shoos you out of the square`,
|
|
117
|
-
` · a same-named participant stands here — the name is taken`,
|
|
118
|
-
` · --kick banishes her and the name becomes yours`,
|
|
119
|
-
`» ${participantCommandPrefix(context.squarePath, joinedName)} join --kick`,
|
|
120
|
-
].join('\n'));
|
|
121
|
-
}
|
|
122
|
-
recordLocalJoin(joinedName, context.squarePath);
|
|
123
|
-
await sweepPendingNotifications(context.squarePath);
|
|
124
|
-
const fallback = hasAutomaticDeliveryIdentity()
|
|
125
|
-
? ''
|
|
126
|
-
: `\n» ${participantCommandPrefix(context.squarePath, joinedName)} catch --idle 30m\n no session delivery detected — keep this catch open for new activity`;
|
|
127
|
-
const line = reconnect && !intent.kick
|
|
128
|
-
? `● you are already in the square`
|
|
129
|
-
: `✓ you banished the original ${joinedName} — the name is yours`;
|
|
130
|
-
const scene = doc.warmup.join('\n').trim();
|
|
131
|
-
const showScene = !(reconnect && !intent.kick) && scene !== '';
|
|
132
|
-
return withPathOutput(context.squarePath, `${line}${showScene ? `\n\n${scene}` : ''}${fallback}`, { participantCount: inSquareCount(doc) });
|
|
129
|
+
finally {
|
|
130
|
+
await square.close();
|
|
133
131
|
}
|
|
134
132
|
},
|
|
135
133
|
present: (result) => process.stdout.write(result),
|
|
@@ -195,10 +193,15 @@ export const doneCommand = {
|
|
|
195
193
|
parse: parseDone,
|
|
196
194
|
async execute(intent, context) {
|
|
197
195
|
const body = resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim();
|
|
198
|
-
const
|
|
199
|
-
const
|
|
196
|
+
const square = await Square.at({ path: context.squarePath, clock: nowMs, notifier: wakeNotifierForSquare(context.squarePath) });
|
|
197
|
+
const participant = await square.join(intent.name);
|
|
198
|
+
const result = await participant.done(body);
|
|
199
|
+
await square.close();
|
|
200
|
+
const name = result.activity.actor;
|
|
200
201
|
recordLocalDone(name, context.squarePath);
|
|
201
|
-
|
|
202
|
+
const presentation = await openSquare(context.squarePath, { clock: nowMs });
|
|
203
|
+
const participantCount = (await entryPresentation(presentation, name).finally(() => closeOpenSquare(presentation))).participantCount;
|
|
204
|
+
return withPathOutput(context.squarePath, `○ ${participantIdentity(name)} steps out of the square — done · just now`, { participantCount });
|
|
202
205
|
},
|
|
203
206
|
present: (result) => process.stdout.write(result),
|
|
204
207
|
};
|
|
@@ -210,9 +213,19 @@ function parseHold(argv, context) {
|
|
|
210
213
|
export const holdCommand = {
|
|
211
214
|
parse: parseHold,
|
|
212
215
|
async execute(intent, context) {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
const square = await Square.at({ path: context.squarePath, clock: nowMs, notifier: wakeNotifierForSquare(context.squarePath) });
|
|
217
|
+
try {
|
|
218
|
+
const participant = await square.join(intent.name);
|
|
219
|
+
const result = await participant.hold(resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim());
|
|
220
|
+
await square.close();
|
|
221
|
+
const presentationSquare = await openSquare(context.squarePath, { clock: nowMs });
|
|
222
|
+
const presentation = await eventPresentation(presentationSquare, result.activity.id);
|
|
223
|
+
await closeOpenSquare(presentationSquare);
|
|
224
|
+
return withPathOutput(context.squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount, held: true });
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
await square.close();
|
|
228
|
+
}
|
|
216
229
|
},
|
|
217
230
|
present: (result) => process.stdout.write(result),
|
|
218
231
|
};
|
|
@@ -223,25 +236,19 @@ export const resumeCommand = {
|
|
|
223
236
|
return { name: requireParticipant(context.name) };
|
|
224
237
|
},
|
|
225
238
|
async execute(intent, context) {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
keep = parsePositiveInteger(requireValue(argv, index, argv[index]), argv[index]);
|
|
239
|
-
index += 1;
|
|
239
|
+
const square = await Square.at({ path: context.squarePath, clock: nowMs, notifier: wakeNotifierForSquare(context.squarePath) });
|
|
240
|
+
try {
|
|
241
|
+
const participant = await square.join(intent.name);
|
|
242
|
+
const result = await participant.resume();
|
|
243
|
+
await square.close();
|
|
244
|
+
const presentationSquare = await openSquare(context.squarePath, { clock: nowMs });
|
|
245
|
+
const presentation = await eventPresentation(presentationSquare, result.activity.id);
|
|
246
|
+
await closeOpenSquare(presentationSquare);
|
|
247
|
+
return withPathOutput(context.squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount });
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
await square.close();
|
|
240
251
|
}
|
|
241
|
-
return { keep };
|
|
242
|
-
},
|
|
243
|
-
async execute(intent, context) {
|
|
244
|
-
await cmdCompact(context.squarePath, intent);
|
|
245
252
|
},
|
|
246
|
-
present: () =>
|
|
253
|
+
present: (result) => process.stdout.write(result),
|
|
247
254
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { InboxMembership } from './model.js';
|
|
2
|
+
export interface CodexHookInput {
|
|
3
|
+
session_id?: unknown;
|
|
4
|
+
hook_event_name?: unknown;
|
|
5
|
+
cwd?: unknown;
|
|
6
|
+
source?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export declare function codexHookResponse(input: CodexHookInput, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<object | undefined>;
|
|
9
|
+
export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
|
10
|
+
export declare function runCodexHookAsync(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
package/dist/codex-hook.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { presentPendingAtBoundary } from './boundary-presentation.js';
|
|
2
2
|
import { sessionInbox } from './inbox.js';
|
|
3
|
-
|
|
3
|
+
import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
|
|
4
|
+
export async function codexHookResponse(input, lookup = sessionInbox, env = process.env) {
|
|
4
5
|
if (typeof input.session_id !== 'string' || input.session_id === '')
|
|
5
6
|
return undefined;
|
|
6
7
|
if (input.hook_event_name !== 'PostToolUse')
|
|
7
8
|
return undefined;
|
|
8
9
|
return presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }), lookup, env);
|
|
9
10
|
}
|
|
10
|
-
export function runCodexHook(inputText, env = process.env) {
|
|
11
|
+
export async function runCodexHook(inputText, env = process.env) {
|
|
11
12
|
let input;
|
|
12
13
|
try {
|
|
13
14
|
input = JSON.parse(inputText);
|
|
@@ -17,6 +18,37 @@ export function runCodexHook(inputText, env = process.env) {
|
|
|
17
18
|
}
|
|
18
19
|
if (input === null || typeof input !== 'object')
|
|
19
20
|
return '';
|
|
20
|
-
const response = codexHookResponse(input, sessionInbox, env);
|
|
21
|
+
const response = await codexHookResponse(input, sessionInbox, env);
|
|
21
22
|
return response === undefined ? '' : `${JSON.stringify(response)}\n`;
|
|
22
23
|
}
|
|
24
|
+
export async function runCodexHookAsync(inputText, env = process.env) {
|
|
25
|
+
let input;
|
|
26
|
+
try {
|
|
27
|
+
input = JSON.parse(inputText);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
if (input === null || typeof input !== 'object')
|
|
33
|
+
return '';
|
|
34
|
+
const value = input;
|
|
35
|
+
if (typeof value.session_id !== 'string' || typeof value.cwd !== 'string')
|
|
36
|
+
return runCodexHook(inputText, env);
|
|
37
|
+
if (value.hook_event_name === 'SessionStart' || value.hook_event_name === 'SessionResume') {
|
|
38
|
+
try {
|
|
39
|
+
const context = await automaticSessionStart('codex', value.session_id, value.cwd, env);
|
|
40
|
+
return context === undefined ? '' : `${JSON.stringify({ hookSpecificOutput: { hookEventName: value.hook_event_name, additionalContext: context } })}\n`;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (value.hook_event_name === 'SessionEnd') {
|
|
47
|
+
try {
|
|
48
|
+
await automaticSessionEnd('codex', value.session_id, value.cwd, env);
|
|
49
|
+
}
|
|
50
|
+
catch { /* end remains bounded */ }
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
return runCodexHook(inputText, env);
|
|
54
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type ActivitiesOptions, type Act, type StoredAct, type SquareState, type Reach, type HardCap } from './model.js';
|
|
2
|
+
import { peerPublicActs, peerRoomChanges } from './activity-feed.js';
|
|
3
|
+
export interface UnreadActivitySummary {
|
|
4
|
+
name: string;
|
|
5
|
+
count: number;
|
|
6
|
+
latestActivityAgeMs: number;
|
|
7
|
+
previews: UnreadActivityPreview[];
|
|
8
|
+
}
|
|
9
|
+
export interface UnreadActivityPreview {
|
|
10
|
+
number: number;
|
|
11
|
+
act: Extract<StoredAct, {
|
|
12
|
+
kind: 'say';
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
export declare function resolveKnownName(squareState: SquareState, name: string): string;
|
|
16
|
+
export interface DecideJoinResult {
|
|
17
|
+
joinedName: string;
|
|
18
|
+
addParticipant: boolean;
|
|
19
|
+
joinAct?: Extract<Act, {
|
|
20
|
+
kind: 'join';
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export declare function decideJoin(squareState: SquareState, name: string, now: number): DecideJoinResult;
|
|
24
|
+
export type ActDecision = {
|
|
25
|
+
type: 'sent';
|
|
26
|
+
act: Act;
|
|
27
|
+
confirmation: string;
|
|
28
|
+
ownActCount: number;
|
|
29
|
+
pendingPublic: ReturnType<typeof peerPublicActs>;
|
|
30
|
+
pendingRoomChanges: ReturnType<typeof peerRoomChanges>;
|
|
31
|
+
} | {
|
|
32
|
+
type: 'blocked';
|
|
33
|
+
activitySummaries: UnreadActivitySummary[];
|
|
34
|
+
unreadRoomChanges: ReturnType<typeof peerRoomChanges>;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'capped';
|
|
37
|
+
count: number;
|
|
38
|
+
hardCap: number;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'throttled';
|
|
41
|
+
delayMs: number;
|
|
42
|
+
} | {
|
|
43
|
+
type: 'held';
|
|
44
|
+
reason: string | undefined;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'bell_quota';
|
|
47
|
+
nextAt: number;
|
|
48
|
+
};
|
|
49
|
+
export declare function decideAct(squareState: SquareState, input: {
|
|
50
|
+
name: string;
|
|
51
|
+
body: string;
|
|
52
|
+
force: boolean;
|
|
53
|
+
now: number;
|
|
54
|
+
reach?: Reach;
|
|
55
|
+
reply?: number;
|
|
56
|
+
}): ActDecision;
|
|
57
|
+
export declare function coreDone(squareState: SquareState, name: string, body: string, now: number): Extract<Act, {
|
|
58
|
+
kind: 'done';
|
|
59
|
+
}>;
|
|
60
|
+
export declare function coreHold(squareState: SquareState, actor: string, body: string, now: number): Extract<Act, {
|
|
61
|
+
kind: 'hold';
|
|
62
|
+
}>;
|
|
63
|
+
export declare function coreResume(squareState: SquareState, actor: string, now: number): Extract<Act, {
|
|
64
|
+
kind: 'resume';
|
|
65
|
+
}>;
|
|
66
|
+
export interface ParticipantStatus {
|
|
67
|
+
name: string;
|
|
68
|
+
state: 'active' | 'done' | 'not joined';
|
|
69
|
+
activityCount: number;
|
|
70
|
+
lastActiveAt: number | undefined;
|
|
71
|
+
presence: CorePresenceState;
|
|
72
|
+
presenceAt: number | undefined;
|
|
73
|
+
unreadActivityCount: number;
|
|
74
|
+
pendingMentionCount: number;
|
|
75
|
+
}
|
|
76
|
+
export type CorePresenceState = 'never-joined' | 'active' | 'watching' | 'done';
|
|
77
|
+
export interface StatusResult {
|
|
78
|
+
hardCap: HardCap;
|
|
79
|
+
throttlePerMinute: number | undefined;
|
|
80
|
+
activeCount: number;
|
|
81
|
+
doneCount: number;
|
|
82
|
+
holdActive: boolean;
|
|
83
|
+
holdReason: string | undefined;
|
|
84
|
+
holdActor: string | undefined;
|
|
85
|
+
holdAt: number | undefined;
|
|
86
|
+
participants: ParticipantStatus[];
|
|
87
|
+
latestAct: StoredAct | undefined;
|
|
88
|
+
now: number;
|
|
89
|
+
}
|
|
90
|
+
export declare function coreStatus(squareState: SquareState, now: number): StatusResult;
|
|
91
|
+
export declare function coreParticipants(squareState: SquareState, now: number): ParticipantStatus[];
|
|
92
|
+
export declare function coreActivities(squareState: SquareState, opts: ActivitiesOptions): StoredAct[];
|
package/dist/decisions.js
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
import { SquareError, sameName, validateName, } from './model.js';
|
|
2
|
+
import { participantIdentity } from './participant-identity.js';
|
|
2
3
|
import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, getReadState, publicActs, readCursor, resolveRosterName, rosterNames, matchesMentionTarget, THROTTLE_WINDOW_MS, } from './runtime.js';
|
|
3
4
|
import { actDelta, peerPublicActs, peerRoomChanges } from './activity-feed.js';
|
|
4
5
|
import { extractMentions, formatActivityId, validate } from './square-core.js';
|
|
5
6
|
import { deriveDeliveryModel } from './delivery.js';
|
|
6
7
|
import { compileSearchPattern } from './search.js';
|
|
7
|
-
export function resolveKnownName(
|
|
8
|
+
export function resolveKnownName(squareState, name) {
|
|
8
9
|
validateName(name);
|
|
9
|
-
const known = resolveRosterName(
|
|
10
|
+
const known = resolveRosterName(squareState, name);
|
|
10
11
|
if (known === undefined) {
|
|
11
|
-
const roster = rosterNames(
|
|
12
|
-
throw new SquareError('invalid_args', `Unknown participant "${name}". Expected one of: ${roster.join(', ')}.`);
|
|
12
|
+
const roster = rosterNames(squareState);
|
|
13
|
+
throw new SquareError('invalid_args', `Unknown participant "${participantIdentity(name)}". Expected one of: ${roster.map(participantIdentity).join(', ')}.`);
|
|
13
14
|
}
|
|
14
15
|
return known;
|
|
15
16
|
}
|
|
16
17
|
function participantState(state, name) {
|
|
17
18
|
return state.participants.find((participant) => sameName(participant.name, name));
|
|
18
19
|
}
|
|
19
|
-
export function decideJoin(
|
|
20
|
-
|
|
20
|
+
export function decideJoin(squareState, name, now) {
|
|
21
|
+
validateName(name);
|
|
22
|
+
const knownName = resolveRosterName(squareState, name);
|
|
21
23
|
const joinedName = knownName ?? name;
|
|
22
|
-
const state = foldedState(
|
|
24
|
+
const state = foldedState(squareState);
|
|
23
25
|
const result = validate(state, { kind: 'join', actor: joinedName, at: now });
|
|
24
26
|
if (!result.ok && result.reason === 'already_joined') {
|
|
25
|
-
|
|
27
|
+
return { joinedName, addParticipant: false };
|
|
26
28
|
}
|
|
27
29
|
return {
|
|
28
30
|
joinedName,
|
|
@@ -30,22 +32,39 @@ export function decideJoin(doc, name, now) {
|
|
|
30
32
|
joinAct: { kind: 'join', actor: joinedName, at: now },
|
|
31
33
|
};
|
|
32
34
|
}
|
|
35
|
+
function resolveStandingName(squareState, requestedName) {
|
|
36
|
+
validateName(requestedName);
|
|
37
|
+
const name = resolveRosterName(squareState, requestedName);
|
|
38
|
+
if (name === undefined)
|
|
39
|
+
throw new SquareError('not_joined', `${participantIdentity(requestedName)} has not joined this square`);
|
|
40
|
+
return name;
|
|
41
|
+
}
|
|
42
|
+
function requireStanding(squareState, act) {
|
|
43
|
+
const result = validate(foldedState(squareState), act);
|
|
44
|
+
if (result.ok)
|
|
45
|
+
return;
|
|
46
|
+
if (result.reason === 'done')
|
|
47
|
+
throw new SquareError('already_done', `${act.actor === undefined ? 'participant' : participantIdentity(act.actor)} is already done`);
|
|
48
|
+
if (result.reason === 'not_joined')
|
|
49
|
+
throw new SquareError('not_joined', `${act.actor === undefined ? 'participant' : participantIdentity(act.actor)} has not joined this square`);
|
|
50
|
+
throw new Error(`Unexpected standing validation result: ${result.reason}`);
|
|
51
|
+
}
|
|
33
52
|
const UNREAD_PREVIEW_LIMIT = 3;
|
|
34
|
-
export function decideAct(
|
|
53
|
+
export function decideAct(squareState, input) {
|
|
35
54
|
const { now, force } = input;
|
|
36
|
-
const name =
|
|
55
|
+
const name = resolveStandingName(squareState, input.name);
|
|
37
56
|
const body = input.body;
|
|
38
57
|
if (body.trim() === '')
|
|
39
58
|
throw new SquareError('invalid_args', 'express body cannot be empty');
|
|
40
59
|
const reach = input.reach;
|
|
41
60
|
const reply = input.reply;
|
|
42
61
|
if (reply !== undefined) {
|
|
43
|
-
if (!Number.isSafeInteger(reply) || reply < 0 || reply >=
|
|
62
|
+
if (!Number.isSafeInteger(reply) || reply < 0 || reply >= squareState.runtime.nextActIndex) {
|
|
44
63
|
const label = Number.isSafeInteger(reply) && reply >= 0 ? formatActivityId(reply) : String(reply);
|
|
45
64
|
throw new SquareError('invalid_args', `Unknown reply activity: ${label}`);
|
|
46
65
|
}
|
|
47
66
|
}
|
|
48
|
-
const state = foldedState(
|
|
67
|
+
const state = foldedState(squareState);
|
|
49
68
|
if (reach !== 'bell'
|
|
50
69
|
&& extractMentions(body).length === 0) {
|
|
51
70
|
throw new SquareError('invalid_args', 'express requires an @mention unless using --bell');
|
|
@@ -55,10 +74,10 @@ export function decideAct(doc, input) {
|
|
|
55
74
|
kind: 'say', actor: name, at: now, body,
|
|
56
75
|
...(reach !== undefined ? { reach } : {}),
|
|
57
76
|
...(reply !== undefined ? { reply } : {}),
|
|
58
|
-
}, { hardCap:
|
|
77
|
+
}, { hardCap: squareState.hardCap, throttlePerMinute: squareState.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
|
|
59
78
|
if (!result.ok) {
|
|
60
79
|
if (result.reason === 'done')
|
|
61
|
-
throw new SquareError('
|
|
80
|
+
throw new SquareError('already_done', `${name} is already done`);
|
|
62
81
|
if (result.reason === 'held')
|
|
63
82
|
return { type: 'held', reason: result.hold.reason };
|
|
64
83
|
if (result.reason === 'hard_cap')
|
|
@@ -68,9 +87,9 @@ export function decideAct(doc, input) {
|
|
|
68
87
|
if (result.reason === 'bell_quota')
|
|
69
88
|
return { type: 'bell_quota', nextAt: result.nextAt };
|
|
70
89
|
if (result.reason === 'not_joined')
|
|
71
|
-
throw new SquareError('
|
|
90
|
+
throw new SquareError('not_joined', `${name} has not joined this square`);
|
|
72
91
|
}
|
|
73
|
-
const delta = actDelta(
|
|
92
|
+
const delta = actDelta(squareState.acts, readCursor(squareState, name));
|
|
74
93
|
const unreadPublic = peerPublicActs(delta, name);
|
|
75
94
|
const unreadRoomChanges = peerRoomChanges(delta, name);
|
|
76
95
|
const sayCountByActor = new Map();
|
|
@@ -122,21 +141,29 @@ export function decideAct(doc, input) {
|
|
|
122
141
|
pendingRoomChanges: unreadRoomChanges,
|
|
123
142
|
};
|
|
124
143
|
}
|
|
125
|
-
export function coreDone(
|
|
126
|
-
const resolvedName =
|
|
127
|
-
|
|
144
|
+
export function coreDone(squareState, name, body, now) {
|
|
145
|
+
const resolvedName = resolveStandingName(squareState, name);
|
|
146
|
+
const act = { kind: 'done', actor: resolvedName, at: now, body: body.replace(/\r\n/g, '\n').trim() };
|
|
147
|
+
requireStanding(squareState, act);
|
|
148
|
+
return act;
|
|
128
149
|
}
|
|
129
|
-
export function coreHold(
|
|
130
|
-
|
|
150
|
+
export function coreHold(squareState, actor, body, now) {
|
|
151
|
+
const resolvedName = resolveStandingName(squareState, actor);
|
|
152
|
+
const act = { kind: 'hold', actor: resolvedName, at: now, body: body.replace(/\r\n/g, '\n').trim() };
|
|
153
|
+
requireStanding(squareState, act);
|
|
154
|
+
return act;
|
|
131
155
|
}
|
|
132
|
-
export function coreResume(
|
|
133
|
-
|
|
156
|
+
export function coreResume(squareState, actor, now) {
|
|
157
|
+
const resolvedName = resolveStandingName(squareState, actor);
|
|
158
|
+
const act = { kind: 'resume', actor: resolvedName, at: now };
|
|
159
|
+
requireStanding(squareState, act);
|
|
160
|
+
return act;
|
|
134
161
|
}
|
|
135
|
-
function presenceFor(
|
|
162
|
+
function presenceFor(squareState, snapshot, name, now) {
|
|
136
163
|
if (snapshot?.done)
|
|
137
164
|
return { state: 'done', lastAt: snapshot.lastActiveAt };
|
|
138
|
-
const cursor = getReadState(
|
|
139
|
-
const lease = freshWatchLease(
|
|
165
|
+
const cursor = getReadState(squareState, name);
|
|
166
|
+
const lease = freshWatchLease(squareState, name, now);
|
|
140
167
|
if (lease !== undefined)
|
|
141
168
|
return { state: 'watching', lastAt: cursor?.updatedAt ?? lease.heartbeatAt };
|
|
142
169
|
const lastAt = cursor?.updatedAt ?? (snapshot?.joined ? snapshot.lastActiveAt : undefined);
|
|
@@ -144,16 +171,16 @@ function presenceFor(doc, snapshot, name, now) {
|
|
|
144
171
|
? { state: 'never-joined', lastAt: undefined }
|
|
145
172
|
: { state: 'active', lastAt };
|
|
146
173
|
}
|
|
147
|
-
function buildParticipantStatuses(
|
|
148
|
-
const delivery = deriveDeliveryModel(
|
|
174
|
+
function buildParticipantStatuses(squareState, now, state = foldedState(squareState)) {
|
|
175
|
+
const delivery = deriveDeliveryModel(squareState);
|
|
149
176
|
return state.participants.map((snapshot) => {
|
|
150
177
|
const participant = snapshot.name;
|
|
151
|
-
const presence = presenceFor(
|
|
178
|
+
const presence = presenceFor(squareState, snapshot, participant, now);
|
|
152
179
|
const participantStatus = snapshot?.done ? 'done' : snapshot?.joined ? 'active' : 'not joined';
|
|
153
|
-
const consumedThrough = readCursor(
|
|
180
|
+
const consumedThrough = readCursor(squareState, participant);
|
|
154
181
|
let unreadActivityCount = 0;
|
|
155
182
|
if (snapshot?.joined) {
|
|
156
|
-
for (const act of
|
|
183
|
+
for (const act of squareState.acts) {
|
|
157
184
|
if (actStableIndex(act) <= consumedThrough || act.actor === undefined || sameName(act.actor, participant))
|
|
158
185
|
continue;
|
|
159
186
|
if (act.kind !== 'read')
|
|
@@ -172,43 +199,44 @@ function buildParticipantStatuses(doc, now, state = foldedState(doc)) {
|
|
|
172
199
|
};
|
|
173
200
|
});
|
|
174
201
|
}
|
|
175
|
-
export function coreStatus(
|
|
176
|
-
const state = foldedState(
|
|
177
|
-
const latestAct = publicActs(
|
|
202
|
+
export function coreStatus(squareState, now) {
|
|
203
|
+
const state = foldedState(squareState);
|
|
204
|
+
const latestAct = publicActs(squareState.acts).at(-1);
|
|
178
205
|
return {
|
|
179
|
-
hardCap:
|
|
180
|
-
throttlePerMinute:
|
|
206
|
+
hardCap: squareState.hardCap,
|
|
207
|
+
throttlePerMinute: squareState.throttlePerMinute,
|
|
181
208
|
activeCount: state.joined.length,
|
|
182
209
|
doneCount: state.done.length,
|
|
183
210
|
holdActive: state.hold.active,
|
|
184
211
|
holdReason: state.hold.reason,
|
|
185
212
|
holdActor: state.hold.actor,
|
|
186
213
|
holdAt: state.hold.at,
|
|
187
|
-
participants: buildParticipantStatuses(
|
|
214
|
+
participants: buildParticipantStatuses(squareState, now, state),
|
|
188
215
|
latestAct,
|
|
189
216
|
now,
|
|
190
217
|
};
|
|
191
218
|
}
|
|
192
|
-
export function coreParticipants(
|
|
193
|
-
return buildParticipantStatuses(
|
|
219
|
+
export function coreParticipants(squareState, now) {
|
|
220
|
+
return buildParticipantStatuses(squareState, now);
|
|
194
221
|
}
|
|
195
|
-
export function coreActivities(
|
|
222
|
+
export function coreActivities(squareState, opts) {
|
|
196
223
|
const participants = opts.participants ?? [];
|
|
197
|
-
const canonicalParticipants = participants.map((participant) => resolveKnownName(
|
|
198
|
-
const viewer = opts.viewer !== undefined ? resolveKnownName(
|
|
199
|
-
let acts = [...
|
|
200
|
-
// --at establishes
|
|
201
|
-
if (opts.
|
|
224
|
+
const canonicalParticipants = participants.map((participant) => resolveKnownName(squareState, participant));
|
|
225
|
+
const viewer = opts.viewer !== undefined ? resolveKnownName(squareState, opts.viewer) : undefined;
|
|
226
|
+
let acts = [...squareState.acts];
|
|
227
|
+
// --at establishes one or more context windows first; other filters AND inside their union.
|
|
228
|
+
if (opts.atIndexes !== undefined && opts.atIndexes.length > 0) {
|
|
202
229
|
const before = opts.beforeContext ?? 0;
|
|
203
230
|
const after = opts.afterContext ?? 0;
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
231
|
+
const selected = new Set();
|
|
232
|
+
for (const atIndex of opts.atIndexes) {
|
|
233
|
+
const centerPos = acts.findIndex((act) => act.index === atIndex);
|
|
234
|
+
if (centerPos < 0)
|
|
235
|
+
continue;
|
|
236
|
+
for (const act of acts.slice(Math.max(0, centerPos - before), centerPos + after + 1))
|
|
237
|
+
selected.add(act.index);
|
|
238
|
+
}
|
|
239
|
+
acts = acts.filter((act) => selected.has(act.index));
|
|
212
240
|
}
|
|
213
241
|
if (opts.afterIndex != null)
|
|
214
242
|
acts = acts.filter((act) => act.index > opts.afterIndex);
|
|
@@ -220,13 +248,13 @@ export function coreActivities(doc, opts) {
|
|
|
220
248
|
if (opts.after != null)
|
|
221
249
|
acts = acts.filter((act) => act.at > opts.after);
|
|
222
250
|
if (opts.mention != null) {
|
|
223
|
-
const mention = resolveKnownName(
|
|
251
|
+
const mention = resolveKnownName(squareState, opts.mention);
|
|
224
252
|
acts = acts.filter((act) => act.kind === 'say' && (act.reach === 'bell' || matchesMentionTarget(act, mention)));
|
|
225
253
|
}
|
|
226
254
|
if (opts.pending) {
|
|
227
255
|
if (viewer === undefined)
|
|
228
256
|
return [];
|
|
229
|
-
const pendingIndexes = new Set(deriveDeliveryModel(
|
|
257
|
+
const pendingIndexes = new Set(deriveDeliveryModel(squareState).pendingFor(viewer).map((notification) => notification.item.index));
|
|
230
258
|
acts = acts.filter((act) => pendingIndexes.has(act.index));
|
|
231
259
|
}
|
|
232
260
|
const search = opts.grep !== undefined ? { pattern: opts.grep, fixed: false } : opts.fixed !== undefined ? { pattern: opts.fixed, fixed: true } : undefined;
|
|
@@ -249,25 +277,3 @@ export function coreActivities(doc, opts) {
|
|
|
249
277
|
}
|
|
250
278
|
return acts;
|
|
251
279
|
}
|
|
252
|
-
export function coreCompact(doc, keep) {
|
|
253
|
-
if (doc.acts.length <= keep)
|
|
254
|
-
return { archived: [], doc };
|
|
255
|
-
const splitAt = doc.acts.length - keep;
|
|
256
|
-
const archived = doc.acts.slice(0, splitAt);
|
|
257
|
-
const retained = doc.acts.slice(splitAt);
|
|
258
|
-
const cutoffIndex = actStableIndex(archived[archived.length - 1]);
|
|
259
|
-
const unread = foldedState(doc).participants
|
|
260
|
-
.filter((participant) => participant.joined && readCursor(doc, participant.name) < cutoffIndex)
|
|
261
|
-
.map((participant) => participant.name);
|
|
262
|
-
if (unread.length > 0) {
|
|
263
|
-
throw new SquareError('conflict', `Refusing to compact: ${unread.join(', ')} ${unread.length === 1 ? 'has' : 'have'} not read through the activities being archived.`);
|
|
264
|
-
}
|
|
265
|
-
return {
|
|
266
|
-
archived,
|
|
267
|
-
doc: {
|
|
268
|
-
...doc,
|
|
269
|
-
acts: retained,
|
|
270
|
-
runtime: doc.runtime,
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
}
|