@astrosheep/square 0.3.5 → 0.3.7
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 +3 -2
- package/codex-plugin/hooks/hooks.json +3 -14
- package/dist/activity-feed.js +26 -18
- package/dist/activity.js +10 -10
- package/dist/artifact.js +138 -203
- package/dist/boundary-presentation.js +77 -0
- package/dist/claude-hook.js +4 -94
- package/dist/cli/context.js +7 -7
- package/dist/cli/maintenance-commands.js +10 -26
- package/dist/cli/meta-commands.js +3 -6
- package/dist/cli/observation-commands.js +48 -53
- package/dist/cli/program.js +4 -4
- package/dist/cli/registry.js +5 -5
- package/dist/cli/square-commands.js +27 -20
- package/dist/cmd/notify-once.js +23 -21
- package/dist/codex-hook.js +22 -0
- package/dist/compact.js +1 -1
- package/dist/decisions.js +61 -88
- package/dist/delivery-health.js +104 -210
- package/dist/delivery.js +68 -18
- package/dist/doctor.js +9 -8
- package/dist/harness-claude.js +38 -245
- package/dist/harness-codex.js +82 -616
- package/dist/harness-stage.js +36 -0
- package/dist/harness.js +3 -5
- package/dist/help.js +43 -35
- package/dist/inbox.js +12 -11
- package/dist/index.js +10 -121
- package/dist/list.js +1 -1
- package/dist/model.js +0 -6
- package/dist/notification-failures.js +54 -0
- package/dist/notifications.js +47 -62
- package/dist/paseo-delivery.js +160 -0
- package/dist/paseo-state.js +31 -0
- package/dist/paseo-timeline.js +58 -188
- package/dist/presentation.js +57 -64
- package/dist/presented.js +9 -8
- package/dist/registry.js +55 -45
- package/dist/runtime.js +27 -84
- package/dist/square-application.js +135 -130
- package/dist/square-core.js +3 -11
- package/dist/stream.js +27 -126
- package/dist/wake-sink.js +3 -214
- package/dist/watch.js +65 -122
- package/extensions/square-opencode.js +8 -73
- package/extensions/square-pi.js +8 -132
- package/guides/architect.md +3 -3
- package/guides/participant.md +25 -16
- package/package.json +2 -2
- package/skills/brainstorm/SKILL.md +25 -32
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +39 -107
- package/skills/square/hooks/hooks.json +2 -13
- package/skills/square-feedback/SKILL.md +4 -4
- package/dist/harness-lifecycle.js +0 -102
- package/dist/square-store.js +0 -111
- package/dist/terminal.js +0 -125
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { cmdActivity } from '../activity.js';
|
|
2
2
|
import { loadSquare } from '../artifact.js';
|
|
3
3
|
import { cmdCompact } from '../compact.js';
|
|
4
|
-
import { SquareError, formatHardCap,
|
|
5
|
-
import { participantCommandPrefix, quoteShell, renderEventCli, renderPublicTail,
|
|
4
|
+
import { SquareError, formatHardCap, validateName, } from '../model.js';
|
|
5
|
+
import { participantCommandPrefix, quoteShell, renderEventCli, renderPublicTail, withPathOutput, } from '../presentation.js';
|
|
6
6
|
import { hasAutomaticDeliveryIdentity, recordLocalDone, recordLocalJoin } from '../registry.js';
|
|
7
7
|
import { inSquareCount, isCurrentlyJoined, nowMs, resolveRosterName } from '../runtime.js';
|
|
8
8
|
import { createSquare, execute } from '../square-application.js';
|
|
9
9
|
import { fail, parseHardCap, parsePositiveInteger, readPipedBodyFallback, readStdinSync, requireParticipant, requireValue, resolveBody, usage, } from './context.js';
|
|
10
10
|
function parseBuild(argv) {
|
|
11
|
-
const options = { force: false };
|
|
11
|
+
const options = { force: false, hardCap: null };
|
|
12
12
|
for (let index = 0; index < argv.length; index++) {
|
|
13
13
|
const flag = argv[index];
|
|
14
14
|
switch (flag) {
|
|
@@ -36,23 +36,21 @@ function parseBuild(argv) {
|
|
|
36
36
|
if (options.template !== undefined && !/^[a-zA-Z0-9-]+$/.test(options.template)) {
|
|
37
37
|
fail('Invalid template name: only letters, digits, and hyphens allowed.');
|
|
38
38
|
}
|
|
39
|
-
if (options.hardCap === undefined)
|
|
40
|
-
fail('Missing required build option: --cap must be a positive integer or -1.');
|
|
41
39
|
if (options.throttlePerMinute !== undefined && options.throttlePerMinute <= 0) {
|
|
42
40
|
fail('Invalid build option: --throttle must be a positive integer.');
|
|
43
41
|
}
|
|
44
42
|
const snippet = readStdinSync();
|
|
45
43
|
if (snippet.trim() === '')
|
|
46
44
|
fail('Missing Markdown body snippet on stdin.');
|
|
47
|
-
return { options
|
|
45
|
+
return { options, snippet };
|
|
48
46
|
}
|
|
49
47
|
export const buildCommand = {
|
|
50
48
|
parse: (argv) => parseBuild(argv),
|
|
51
49
|
async execute(intent, context) {
|
|
52
50
|
await createSquare(context.squarePath, intent.options, intent.snippet);
|
|
53
|
-
const cap = formatHardCap(intent.options.hardCap);
|
|
51
|
+
const cap = intent.options.hardCap === null ? 'unlimited' : formatHardCap(intent.options.hardCap);
|
|
54
52
|
const throttle = intent.options.throttlePerMinute === undefined ? [] : [` · throttle ${intent.options.throttlePerMinute}/min`];
|
|
55
|
-
return withPathOutput(context.squarePath, ['✓ built', ` · cap ${cap
|
|
53
|
+
return withPathOutput(context.squarePath, ['✓ built', ` · cap ${cap}`, ...throttle, ' · participants (none seeded — first join adds names)'].join('\n'), { participantCount: 0 });
|
|
56
54
|
},
|
|
57
55
|
present: (result) => process.stdout.write(result),
|
|
58
56
|
};
|
|
@@ -75,7 +73,7 @@ function parseJoin(argv, context) {
|
|
|
75
73
|
export const joinCommand = {
|
|
76
74
|
parse: parseJoin,
|
|
77
75
|
async execute(intent, context) {
|
|
78
|
-
|
|
76
|
+
validateName(intent.name);
|
|
79
77
|
try {
|
|
80
78
|
const committed = await execute(context.squarePath, { type: 'join', name: intent.name, now: nowMs() });
|
|
81
79
|
const joinedName = committed.result.joinedName;
|
|
@@ -95,7 +93,7 @@ export const joinCommand = {
|
|
|
95
93
|
...(isRejoin ? [] : ['', `» ${participantCommandPrefix(context.squarePath, joinedName)} warmup`]),
|
|
96
94
|
...fallback,
|
|
97
95
|
].join('\n');
|
|
98
|
-
return
|
|
96
|
+
return withPathOutput(context.squarePath, output, { participantCount: inSquareCount(after) });
|
|
99
97
|
}
|
|
100
98
|
catch (error) {
|
|
101
99
|
if (!(error instanceof SquareError) || error.code !== 'conflict')
|
|
@@ -108,7 +106,7 @@ export const joinCommand = {
|
|
|
108
106
|
const fallback = hasAutomaticDeliveryIdentity()
|
|
109
107
|
? ''
|
|
110
108
|
: `\n» ${participantCommandPrefix(context.squarePath, joinedName)} catch --idle 30m\n no session delivery detected — keep this catch open for new activity`;
|
|
111
|
-
return
|
|
109
|
+
return withPathOutput(context.squarePath, `● ${joinedName} is already in the square${fallback}`, { participantCount: inSquareCount(doc) });
|
|
112
110
|
}
|
|
113
111
|
},
|
|
114
112
|
present: (result) => process.stdout.write(result),
|
|
@@ -118,6 +116,7 @@ function parseActivity(argv, context) {
|
|
|
118
116
|
let noWait = false;
|
|
119
117
|
let beside;
|
|
120
118
|
let bell = false;
|
|
119
|
+
let reply;
|
|
121
120
|
const bodyArgs = [];
|
|
122
121
|
for (let index = 0; index < argv.length; index++) {
|
|
123
122
|
const argument = argv[index];
|
|
@@ -131,23 +130,30 @@ function parseActivity(argv, context) {
|
|
|
131
130
|
}
|
|
132
131
|
else if (argument === '--bell')
|
|
133
132
|
bell = true;
|
|
133
|
+
else if (argument === '--reply') {
|
|
134
|
+
const value = requireValue(argv, index, argument).trim().match(/^(?:act_)?(\d+)$/i);
|
|
135
|
+
if (!value || !Number.isSafeInteger(Number(value[1])))
|
|
136
|
+
fail('Invalid --reply: expected an activity id like act_12 or 12.');
|
|
137
|
+
reply = Number(value[1]);
|
|
138
|
+
index += 1;
|
|
139
|
+
}
|
|
134
140
|
else
|
|
135
141
|
bodyArgs.push(argument);
|
|
136
142
|
}
|
|
137
143
|
if (bell && beside !== undefined)
|
|
138
|
-
fail('Invalid
|
|
144
|
+
fail('Invalid express options: --beside and --bell are mutually exclusive.');
|
|
139
145
|
const reach = bell ? 'bell' : beside === undefined ? undefined : { beside };
|
|
140
146
|
if (bodyArgs.length !== 1) {
|
|
141
147
|
if (bodyArgs.length === 0) {
|
|
142
148
|
const piped = readPipedBodyFallback();
|
|
143
149
|
if (piped !== undefined)
|
|
144
|
-
return { name: requireParticipant(context.name), activity: piped, force, noWait, reach };
|
|
150
|
+
return { name: requireParticipant(context.name), activity: piped, force, noWait, reach, reply };
|
|
145
151
|
}
|
|
146
|
-
fail("
|
|
152
|
+
fail("express requires a body argument (a quoted string or '-' with piped stdin)");
|
|
147
153
|
}
|
|
148
|
-
return { name: requireParticipant(context.name), activity: bodyArgs[0], force, noWait, reach };
|
|
154
|
+
return { name: requireParticipant(context.name), activity: bodyArgs[0], force, noWait, reach, reply };
|
|
149
155
|
}
|
|
150
|
-
export const
|
|
156
|
+
export const expressCommand = {
|
|
151
157
|
parse: parseActivity,
|
|
152
158
|
async execute(intent, context) {
|
|
153
159
|
const reachArg = intent.reach === 'bell' ? ' --bell' : intent.reach === undefined ? '' : ` --beside ${quoteShell(intent.reach.beside)}`;
|
|
@@ -155,7 +161,8 @@ export const actCommand = {
|
|
|
155
161
|
force: intent.force,
|
|
156
162
|
noWait: intent.noWait,
|
|
157
163
|
reach: intent.reach,
|
|
158
|
-
|
|
164
|
+
reply: intent.reply,
|
|
165
|
+
forceCommand: `${participantCommandPrefix(context.squarePath, intent.name)} express --force${reachArg}${intent.reply === undefined ? '' : ` --reply act_${intent.reply}`} -`,
|
|
159
166
|
});
|
|
160
167
|
},
|
|
161
168
|
present: () => { },
|
|
@@ -170,7 +177,7 @@ export const doneCommand = {
|
|
|
170
177
|
async execute(intent, context) {
|
|
171
178
|
const body = resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim();
|
|
172
179
|
const committed = await execute(context.squarePath, { type: 'done', name: intent.name, body, now: nowMs() });
|
|
173
|
-
const name = committed.acts[0].
|
|
180
|
+
const name = committed.acts[0].actor;
|
|
174
181
|
recordLocalDone(name, context.squarePath);
|
|
175
182
|
return withPathOutput(context.squarePath, `× ${name} steps out of the square — done · just now`, { participantCount: inSquareCount(loadSquare(context.squarePath)) });
|
|
176
183
|
},
|
|
@@ -186,7 +193,7 @@ export const holdCommand = {
|
|
|
186
193
|
async execute(intent, context) {
|
|
187
194
|
const committed = await execute(context.squarePath, { type: 'hold', actor: intent.name, body: resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim(), now: nowMs() });
|
|
188
195
|
const doc = loadSquare(context.squarePath);
|
|
189
|
-
return withPathOutput(context.squarePath, renderEventCli(committed.acts[0]
|
|
196
|
+
return withPathOutput(context.squarePath, renderEventCli(committed.acts[0]), { participantCount: inSquareCount(doc), held: true });
|
|
190
197
|
},
|
|
191
198
|
present: (result) => process.stdout.write(result),
|
|
192
199
|
};
|
|
@@ -199,7 +206,7 @@ export const resumeCommand = {
|
|
|
199
206
|
async execute(intent, context) {
|
|
200
207
|
const committed = await execute(context.squarePath, { type: 'resume', actor: intent.name, now: nowMs() });
|
|
201
208
|
const doc = loadSquare(context.squarePath);
|
|
202
|
-
return withPathOutput(context.squarePath, renderEventCli(committed.acts[0]
|
|
209
|
+
return withPathOutput(context.squarePath, renderEventCli(committed.acts[0]), { participantCount: inSquareCount(doc) });
|
|
203
210
|
},
|
|
204
211
|
present: (result) => process.stdout.write(result),
|
|
205
212
|
};
|
package/dist/cmd/notify-once.js
CHANGED
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
2
|
import { resolve } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
|
+
import { recordNotificationFailure } from '../notification-failures.js';
|
|
5
|
+
import { notificationDeliveryWaitMs, processActNotificationsOnce } from '../notifications.js';
|
|
6
|
+
function args(argv) {
|
|
6
7
|
let squarePath;
|
|
7
8
|
let actIndex;
|
|
8
|
-
for (let index = 0; index < argv.length; index
|
|
9
|
-
|
|
10
|
-
if (argument === '--square-path' && argv[index + 1] !== undefined) {
|
|
9
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
10
|
+
if (argv[index] === '--square-path' && argv[index + 1] !== undefined)
|
|
11
11
|
squarePath = resolve(argv[++index]);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (Number.isInteger(value) && value >= 0)
|
|
17
|
-
actIndex = value;
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
throw new Error(`Unknown notify-once argument: ${argument}`);
|
|
12
|
+
else if (argv[index] === '--act-index' && /^\d+$/.test(argv[index + 1] ?? ''))
|
|
13
|
+
actIndex = Number(argv[++index]);
|
|
14
|
+
else
|
|
15
|
+
throw new Error(`Unknown notify-once argument: ${argv[index]}`);
|
|
21
16
|
}
|
|
22
|
-
if (
|
|
17
|
+
if (squarePath === undefined || actIndex === undefined)
|
|
23
18
|
throw new Error('notify-once requires --square-path and --act-index.');
|
|
24
|
-
}
|
|
25
19
|
return { squarePath, actIndex };
|
|
26
20
|
}
|
|
27
21
|
async function main() {
|
|
28
|
-
if (process.env
|
|
22
|
+
if (process.env.SQUARE_DISABLE_PASEO_WAKE === '1')
|
|
29
23
|
return;
|
|
30
|
-
const { squarePath, actIndex } =
|
|
24
|
+
const { squarePath, actIndex } = args(process.argv.slice(2));
|
|
31
25
|
await sleep(notificationDeliveryWaitMs());
|
|
32
26
|
await processActNotificationsOnce(squarePath, actIndex);
|
|
33
27
|
}
|
|
34
|
-
main().catch(() => {
|
|
35
|
-
|
|
28
|
+
main().catch((error) => {
|
|
29
|
+
const squarePath = process.argv.includes('--square-path') ? process.argv[process.argv.indexOf('--square-path') + 1] : undefined;
|
|
30
|
+
if (squarePath) {
|
|
31
|
+
recordNotificationFailure(squarePath, {
|
|
32
|
+
actIndex: Number(process.argv[process.argv.indexOf('--act-index') + 1]) || 0,
|
|
33
|
+
sink: 'worker',
|
|
34
|
+
message: error instanceof Error ? error.message : String(error),
|
|
35
|
+
diagnostic: { phase: 'worker' },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
36
38
|
process.exitCode = 0;
|
|
37
39
|
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { presentPendingAtBoundary } from './boundary-presentation.js';
|
|
2
|
+
import { sessionInbox } from './inbox.js';
|
|
3
|
+
export function codexHookResponse(input, lookup = sessionInbox, env = process.env) {
|
|
4
|
+
if (typeof input.session_id !== 'string' || input.session_id === '')
|
|
5
|
+
return undefined;
|
|
6
|
+
if (input.hook_event_name !== 'PostToolUse')
|
|
7
|
+
return undefined;
|
|
8
|
+
return presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }), lookup, env);
|
|
9
|
+
}
|
|
10
|
+
export function runCodexHook(inputText, env = process.env) {
|
|
11
|
+
let input;
|
|
12
|
+
try {
|
|
13
|
+
input = JSON.parse(inputText);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
if (input === null || typeof input !== 'object')
|
|
19
|
+
return '';
|
|
20
|
+
const response = codexHookResponse(input, sessionInbox, env);
|
|
21
|
+
return response === undefined ? '' : `${JSON.stringify(response)}\n`;
|
|
22
|
+
}
|
package/dist/compact.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function cmdCompact(squarePath, opts) {
|
|
|
13
13
|
const result = committed.result;
|
|
14
14
|
archivedCount = result.archived.length;
|
|
15
15
|
keptCount = result.doc.acts.length;
|
|
16
|
-
const summary = ['✓ compacted', ` · archived ${archivedCount}
|
|
16
|
+
const summary = ['✓ compacted', ` · archived ${archivedCount} activities`, ` · kept ${keptCount} activities`, ...(archivedCount > 0 ? [` · sidecar ${archive}`] : [])].join('\n');
|
|
17
17
|
process.stdout.write(withPathOutput(squarePath, summary));
|
|
18
18
|
}
|
|
19
19
|
catch (err) {
|
package/dist/decisions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SquareError, sameName, validateName, } from './model.js';
|
|
2
|
-
import { UNREAD_BLOCK_GRACE_MS,
|
|
3
|
-
import {
|
|
2
|
+
import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, getReadState, publicActs, readCursor, resolveRosterName, rosterNames, matchesMentionTarget, THROTTLE_WINDOW_MS, } from './runtime.js';
|
|
3
|
+
import { actDelta, peerPublicActs, peerRoomChanges } from './activity-feed.js';
|
|
4
4
|
import { validate } from './square-core.js';
|
|
5
5
|
import { deriveDeliveryModel } from './delivery.js';
|
|
6
6
|
import { compileSearchPattern } from './search.js';
|
|
@@ -36,14 +36,20 @@ export function decideAct(doc, input) {
|
|
|
36
36
|
const name = resolveKnownName(doc, input.name);
|
|
37
37
|
const body = input.body;
|
|
38
38
|
if (body.trim() === '')
|
|
39
|
-
throw new SquareError('invalid_args', '
|
|
39
|
+
throw new SquareError('invalid_args', 'express body cannot be empty');
|
|
40
40
|
const reach = input.reach;
|
|
41
|
+
const reply = input.reply;
|
|
42
|
+
if (reply !== undefined) {
|
|
43
|
+
if (!Number.isSafeInteger(reply) || reply < 0 || reply >= doc.runtime.nextActIndex) {
|
|
44
|
+
throw new SquareError('invalid_args', `Unknown reply activity: act_${reply}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
41
47
|
const state = foldedState(doc);
|
|
42
48
|
const current = participantState(state, name);
|
|
43
|
-
const result = validate(state, { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}) }, { hardCap: doc.hardCap, throttlePerMinute: doc.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
|
|
49
|
+
const result = validate(state, { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}), ...(reply !== undefined ? { reply } : {}) }, { hardCap: doc.hardCap, throttlePerMinute: doc.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
|
|
44
50
|
if (!result.ok) {
|
|
45
51
|
if (result.reason === 'done')
|
|
46
|
-
throw new SquareError('conflict', `${name} is done; rejoin to
|
|
52
|
+
throw new SquareError('conflict', `${name} is done; rejoin to express again`);
|
|
47
53
|
if (result.reason === 'held')
|
|
48
54
|
return { type: 'held', reason: result.hold.reason };
|
|
49
55
|
if (result.reason === 'hard_cap')
|
|
@@ -55,26 +61,26 @@ export function decideAct(doc, input) {
|
|
|
55
61
|
if (result.reason === 'not_joined')
|
|
56
62
|
throw new SquareError('conflict', `${name} has not joined this square`);
|
|
57
63
|
}
|
|
58
|
-
const delta =
|
|
64
|
+
const delta = actDelta(doc.acts, readCursor(doc, name));
|
|
59
65
|
const unreadPublic = peerPublicActs(delta, name);
|
|
60
66
|
const unreadRoomChanges = peerRoomChanges(delta, name);
|
|
61
67
|
const sayCountByActor = new Map();
|
|
62
68
|
const unreadByParticipant = new Map();
|
|
63
69
|
for (const item of delta) {
|
|
64
|
-
if (item.
|
|
65
|
-
const key = item.
|
|
70
|
+
if (item.kind === 'say') {
|
|
71
|
+
const key = item.actor.toLocaleLowerCase();
|
|
66
72
|
sayCountByActor.set(key, (sayCountByActor.get(key) ?? 0) + 1);
|
|
67
73
|
}
|
|
68
|
-
if (item.
|
|
74
|
+
if (item.kind !== 'say' || sameName(item.actor, name))
|
|
69
75
|
continue;
|
|
70
|
-
const actorKey = item.
|
|
71
|
-
const currentSummary = unreadByParticipant.get(item.
|
|
72
|
-
unreadByParticipant.set(item.
|
|
76
|
+
const actorKey = item.actor.toLocaleLowerCase();
|
|
77
|
+
const currentSummary = unreadByParticipant.get(item.actor);
|
|
78
|
+
unreadByParticipant.set(item.actor, {
|
|
73
79
|
count: (currentSummary?.count ?? 0) + 1,
|
|
74
|
-
latestAt: currentSummary === undefined ? item.
|
|
80
|
+
latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
|
|
75
81
|
previews: [
|
|
76
82
|
...(currentSummary?.previews ?? []),
|
|
77
|
-
{ number: sayCountByActor.get(actorKey) ?? 1, act: item
|
|
83
|
+
{ number: sayCountByActor.get(actorKey) ?? 1, act: item },
|
|
78
84
|
].slice(-UNREAD_PREVIEW_LIMIT),
|
|
79
85
|
});
|
|
80
86
|
}
|
|
@@ -95,7 +101,7 @@ export function decideAct(doc, input) {
|
|
|
95
101
|
const ownActCount = (current?.activityCount ?? 0) + 1;
|
|
96
102
|
return {
|
|
97
103
|
type: 'sent',
|
|
98
|
-
act: { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}) },
|
|
104
|
+
act: { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}), ...(reply !== undefined ? { reply } : {}) },
|
|
99
105
|
confirmation: `● heads turn your way — #${ownActCount}`,
|
|
100
106
|
ownActCount,
|
|
101
107
|
pendingPublic: unreadPublic,
|
|
@@ -112,35 +118,23 @@ export function coreHold(_doc, actor, body, now) {
|
|
|
112
118
|
export function coreResume(_doc, actor, now) {
|
|
113
119
|
return { kind: 'resume', actor, at: now, body: '' };
|
|
114
120
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (lastAt === undefined)
|
|
127
|
-
return { name: participant, state: 'never-joined', lastAt: undefined };
|
|
128
|
-
return { name: participant, state: 'active', lastAt };
|
|
129
|
-
});
|
|
121
|
+
function presenceFor(doc, snapshot, name, now) {
|
|
122
|
+
if (snapshot?.done)
|
|
123
|
+
return { state: 'done', lastAt: snapshot.lastActiveAt };
|
|
124
|
+
const cursor = getReadState(doc, name);
|
|
125
|
+
const lease = freshWatchLease(doc, name, now);
|
|
126
|
+
if (lease !== undefined)
|
|
127
|
+
return { state: 'watching', lastAt: cursor?.updatedAt ?? lease.heartbeatAt };
|
|
128
|
+
const lastAt = cursor?.updatedAt ?? (snapshot?.joined ? snapshot.lastActiveAt : undefined);
|
|
129
|
+
return lastAt === undefined
|
|
130
|
+
? { state: 'never-joined', lastAt: undefined }
|
|
131
|
+
: { state: 'active', lastAt };
|
|
130
132
|
}
|
|
131
133
|
function buildParticipantStatuses(doc, now, state = foldedState(doc)) {
|
|
132
134
|
const delivery = deriveDeliveryModel(doc);
|
|
133
|
-
return state.participants.map((
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const lease = freshWatchLease(doc, participant, now);
|
|
137
|
-
const presence = snapshot?.done
|
|
138
|
-
? { state: 'done', lastAt: snapshot.lastActiveAt }
|
|
139
|
-
: lease !== undefined
|
|
140
|
-
? { state: 'watching', lastAt: cursor?.updatedAt ?? lease.heartbeatAt }
|
|
141
|
-
: cursor !== undefined || snapshot?.joined
|
|
142
|
-
? { state: 'active', lastAt: cursor?.updatedAt ?? snapshot?.lastActiveAt }
|
|
143
|
-
: { state: 'never-joined', lastAt: undefined };
|
|
135
|
+
return state.participants.map((snapshot) => {
|
|
136
|
+
const participant = snapshot.name;
|
|
137
|
+
const presence = presenceFor(doc, snapshot, participant, now);
|
|
144
138
|
const participantStatus = snapshot?.done ? 'done' : snapshot?.joined ? 'active' : 'not joined';
|
|
145
139
|
const consumedThrough = readCursor(doc, participant);
|
|
146
140
|
let unreadActivityCount = 0;
|
|
@@ -170,7 +164,6 @@ export function coreStatus(doc, now) {
|
|
|
170
164
|
return {
|
|
171
165
|
hardCap: doc.hardCap,
|
|
172
166
|
throttlePerMinute: doc.throttlePerMinute,
|
|
173
|
-
participantCount: state.joined.length,
|
|
174
167
|
activeCount: state.joined.length,
|
|
175
168
|
doneCount: state.done.length,
|
|
176
169
|
holdActive: state.hold.active,
|
|
@@ -183,77 +176,62 @@ export function coreStatus(doc, now) {
|
|
|
183
176
|
};
|
|
184
177
|
}
|
|
185
178
|
export function coreParticipants(doc, now) {
|
|
186
|
-
return
|
|
187
|
-
}
|
|
188
|
-
/** True when a say is a bell or explicitly @viewer — not broadcast. */
|
|
189
|
-
function addressesViewer(act, viewer) {
|
|
190
|
-
if (act.kind !== 'say')
|
|
191
|
-
return false;
|
|
192
|
-
if (act.reach === 'bell')
|
|
193
|
-
return true;
|
|
194
|
-
return extractMentions(act.body).some((name) => sameName(name, viewer));
|
|
179
|
+
return buildParticipantStatuses(doc, now);
|
|
195
180
|
}
|
|
196
181
|
export function coreActivities(doc, opts) {
|
|
197
182
|
const participants = opts.participants ?? [];
|
|
198
183
|
const canonicalParticipants = participants.map((participant) => resolveKnownName(doc, participant));
|
|
199
184
|
const viewer = opts.viewer !== undefined ? resolveKnownName(doc, opts.viewer) : undefined;
|
|
200
|
-
let acts = doc.acts
|
|
185
|
+
let acts = [...doc.acts];
|
|
201
186
|
// --at establishes a context window first; other filters AND inside it.
|
|
202
187
|
if (opts.atIndex != null) {
|
|
203
188
|
const before = opts.beforeContext ?? 0;
|
|
204
189
|
const after = opts.afterContext ?? 0;
|
|
205
|
-
const centerPos = acts.findIndex((
|
|
190
|
+
const centerPos = acts.findIndex((act) => act.index === opts.atIndex);
|
|
206
191
|
if (centerPos < 0)
|
|
207
192
|
return [];
|
|
208
193
|
acts = acts.slice(Math.max(0, centerPos - before), centerPos + after + 1);
|
|
209
194
|
}
|
|
210
195
|
if (opts.ids !== undefined && opts.ids.length > 0) {
|
|
211
196
|
const wanted = new Set(opts.ids);
|
|
212
|
-
acts = acts.filter((
|
|
197
|
+
acts = acts.filter((act) => wanted.has(act.index));
|
|
213
198
|
}
|
|
214
199
|
if (opts.afterIndex != null)
|
|
215
|
-
acts = acts.filter((
|
|
200
|
+
acts = acts.filter((act) => act.index > opts.afterIndex);
|
|
216
201
|
if (canonicalParticipants.length > 0) {
|
|
217
|
-
acts = acts.filter((
|
|
218
|
-
(act.actor !== undefined && canonicalParticipants.some((participant) => sameName(participant, act.actor))));
|
|
202
|
+
acts = acts.filter((act) => act.actor !== undefined && canonicalParticipants.some((participant) => sameName(participant, act.actor)));
|
|
219
203
|
}
|
|
220
204
|
if (opts.before != null)
|
|
221
|
-
acts = acts.filter((
|
|
205
|
+
acts = acts.filter((act) => act.at < opts.before);
|
|
222
206
|
if (opts.after != null)
|
|
223
|
-
acts = acts.filter((
|
|
207
|
+
acts = acts.filter((act) => act.at > opts.after);
|
|
224
208
|
if (opts.mention != null) {
|
|
225
209
|
const mention = resolveKnownName(doc, opts.mention);
|
|
226
|
-
acts = acts.filter((
|
|
227
|
-
}
|
|
228
|
-
if (opts.mentionsViewer) {
|
|
229
|
-
if (viewer === undefined)
|
|
230
|
-
return [];
|
|
231
|
-
acts = acts.filter(({ act }) => addressesViewer(act, viewer));
|
|
210
|
+
acts = acts.filter((act) => act.kind === 'say' && (act.reach === 'bell' || matchesMentionTarget(act, mention)));
|
|
232
211
|
}
|
|
233
212
|
if (opts.pending) {
|
|
234
213
|
if (viewer === undefined)
|
|
235
214
|
return [];
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return false;
|
|
239
|
-
if (!isPostJoinActivity(doc.acts, viewer, index))
|
|
240
|
-
return false;
|
|
241
|
-
return !isDeliveryDelivered(doc, viewer, index);
|
|
242
|
-
});
|
|
215
|
+
const pendingIndexes = new Set(deriveDeliveryModel(doc).pendingFor(viewer).map((notification) => notification.item.index));
|
|
216
|
+
acts = acts.filter((act) => pendingIndexes.has(act.index));
|
|
243
217
|
}
|
|
244
218
|
const search = opts.grep !== undefined ? { pattern: opts.grep, fixed: false } : opts.fixed !== undefined ? { pattern: opts.fixed, fixed: true } : undefined;
|
|
245
219
|
if (search !== undefined && search.pattern !== '') {
|
|
246
|
-
// Search
|
|
247
|
-
//
|
|
248
|
-
acts = acts.filter((
|
|
220
|
+
// Search only the public activity model rendered by history, but include all
|
|
221
|
+
// of its user-facing fields rather than coupling matching to rendered text.
|
|
222
|
+
acts = acts.filter((act) => act.kind === 'say' || act.kind === 'done');
|
|
249
223
|
const re = compileSearchPattern(search.pattern, search.fixed);
|
|
250
|
-
acts = acts.filter((
|
|
224
|
+
acts = acts.filter((act) => [
|
|
225
|
+
actId(act.index),
|
|
226
|
+
act.actor ?? '',
|
|
227
|
+
'body' in act && typeof act.body === 'string' ? act.body : '',
|
|
228
|
+
].some((field) => re.test(field)));
|
|
251
229
|
}
|
|
252
230
|
if (opts.order === 'desc') {
|
|
253
|
-
acts = [...acts].sort((a, b) => b.index - a.index || b.
|
|
231
|
+
acts = [...acts].sort((a, b) => b.index - a.index || b.at - a.at);
|
|
254
232
|
}
|
|
255
233
|
else {
|
|
256
|
-
acts = [...acts].sort((a, b) => a.index - b.index || a.
|
|
234
|
+
acts = [...acts].sort((a, b) => a.index - b.index || a.at - b.at);
|
|
257
235
|
}
|
|
258
236
|
return acts;
|
|
259
237
|
}
|
|
@@ -264,23 +242,18 @@ export function coreCompact(doc, keep) {
|
|
|
264
242
|
const archived = doc.acts.slice(0, splitAt);
|
|
265
243
|
const retained = doc.acts.slice(splitAt);
|
|
266
244
|
const cutoffIndex = actStableIndex(archived[archived.length - 1]);
|
|
267
|
-
const unread =
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (!foldedState(doc).participants.some((entry) => sameName(entry.name, participant) && entry.joined))
|
|
271
|
-
return false;
|
|
272
|
-
return readCursor(doc, participant) < cutoffIndex;
|
|
273
|
-
});
|
|
245
|
+
const unread = foldedState(doc).participants
|
|
246
|
+
.filter((participant) => participant.joined && readCursor(doc, participant.name) < cutoffIndex)
|
|
247
|
+
.map((participant) => participant.name);
|
|
274
248
|
if (unread.length > 0) {
|
|
275
|
-
throw new SquareError('conflict', `Refusing to compact: ${unread.join(', ')} ${unread.length === 1 ? 'has' : 'have'} not read through the
|
|
249
|
+
throw new SquareError('conflict', `Refusing to compact: ${unread.join(', ')} ${unread.length === 1 ? 'has' : 'have'} not read through the activities being archived.`);
|
|
276
250
|
}
|
|
277
|
-
const firstActIndex = retained.length > 0 ? actStableIndex(retained[0]) : doc.runtime.nextActIndex;
|
|
278
251
|
return {
|
|
279
252
|
archived,
|
|
280
253
|
doc: {
|
|
281
254
|
...doc,
|
|
282
255
|
acts: retained,
|
|
283
|
-
runtime:
|
|
256
|
+
runtime: doc.runtime,
|
|
284
257
|
},
|
|
285
258
|
};
|
|
286
259
|
}
|