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