@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.
Files changed (52) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/dist/activity-feed.js +26 -18
  3. package/dist/activity.js +23 -22
  4. package/dist/artifact.js +126 -202
  5. package/dist/claude-hook.js +45 -21
  6. package/dist/cli/context.js +143 -0
  7. package/dist/cli/harness-command.js +50 -0
  8. package/dist/cli/maintenance-commands.js +76 -0
  9. package/dist/cli/meta-commands.js +28 -0
  10. package/dist/cli/observation-commands.js +453 -0
  11. package/dist/cli/program.js +48 -0
  12. package/dist/cli/registry.js +40 -0
  13. package/dist/cli/square-commands.js +219 -0
  14. package/dist/cmd/notify-once.js +23 -21
  15. package/dist/compact.js +6 -19
  16. package/dist/decisions.js +53 -86
  17. package/dist/delivery-health.js +104 -210
  18. package/dist/delivery.js +68 -18
  19. package/dist/doctor.js +9 -8
  20. package/dist/harness-claude.js +68 -0
  21. package/dist/harness-codex.js +119 -0
  22. package/dist/harness-links.js +123 -0
  23. package/dist/harness-stage.js +36 -0
  24. package/dist/harness.js +94 -576
  25. package/dist/help.js +44 -35
  26. package/dist/inbox.js +12 -11
  27. package/dist/index.js +30 -129
  28. package/dist/list.js +1 -1
  29. package/dist/model.js +0 -6
  30. package/dist/notification-failures.js +54 -0
  31. package/dist/notifications.js +47 -62
  32. package/dist/paseo-timeline.js +58 -188
  33. package/dist/presentation.js +55 -63
  34. package/dist/presented.js +9 -8
  35. package/dist/registry.js +55 -45
  36. package/dist/runtime.js +26 -137
  37. package/dist/square-application.js +264 -0
  38. package/dist/square-core.js +3 -11
  39. package/dist/square.js +5 -1362
  40. package/dist/stream.js +27 -126
  41. package/dist/wake-sink.js +134 -188
  42. package/dist/watch.js +79 -138
  43. package/extensions/square-opencode.js +1 -1
  44. package/extensions/square-pi.js +8 -130
  45. package/guides/architect.md +3 -3
  46. package/guides/participant.md +25 -16
  47. package/package.json +2 -2
  48. package/skills/brainstorm/SKILL.md +25 -32
  49. package/skills/square/.claude-plugin/plugin.json +1 -1
  50. package/skills/square/SKILL.md +39 -107
  51. package/skills/square-feedback/SKILL.md +4 -4
  52. 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
+ };
@@ -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 { notificationDeliveryWaitMs, processActNotificationsOnce, } from '../notifications.js';
5
- function parseArgs(argv) {
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
- const argument = argv[index];
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
- continue;
13
- }
14
- if (argument === '--act-index' && argv[index + 1] !== undefined) {
15
- const value = Number(argv[++index]);
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 (!squarePath || actIndex === undefined) {
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['SQUARE_DISABLE_PASEO_WAKE'] === '1')
22
+ if (process.env.SQUARE_DISABLE_PASEO_WAKE === '1')
29
23
  return;
30
- const { squarePath, actIndex } = parseArgs(process.argv.slice(2));
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
- // Detached notification delivery must never surface as a CLI failure.
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 { withSquareLock, writeSquareDoc } from './runtime.js';
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 withSquareLock(squarePath, () => {
16
- const doc = loadSquare(squarePath);
17
- const result = coreCompact(doc, opts.keep);
18
- archivedCount = result.archived.length;
19
- keptCount = result.doc.acts.length;
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, actStableIndex, currentHold, foldedState, freshWatchLease, getReadState, publicActs, readCursor, resolveRosterName, rosterNames, extractMentions, matchesMentionTarget, isPostJoinActivity, isDeliveryDelivered, THROTTLE_WINDOW_MS, } from './runtime.js';
3
- import { indexedDelta, peerPublicActs, peerRoomChanges } from './activity-feed.js';
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', 'act body cannot be empty');
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 act again`);
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 = indexedDelta(doc.acts, readCursor(doc, name));
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.act.kind === 'say') {
65
- const key = item.act.actor.toLocaleLowerCase();
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.act.kind !== 'say' || sameName(item.act.actor, name))
68
+ if (item.kind !== 'say' || sameName(item.actor, name))
69
69
  continue;
70
- const actorKey = item.act.actor.toLocaleLowerCase();
71
- const currentSummary = unreadByParticipant.get(item.act.actor);
72
- unreadByParticipant.set(item.act.actor, {
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.act.at : Math.max(currentSummary.latestAt, item.act.at),
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.act },
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
- export function corePresence(doc, now) {
116
- const state = foldedState(doc);
117
- return rosterNames(doc).map((participant) => {
118
- const snapshot = participantState(state, participant);
119
- if (snapshot?.done)
120
- return { name: participant, state: 'done', lastAt: snapshot.lastActiveAt };
121
- const cursor = getReadState(doc, participant);
122
- const lease = freshWatchLease(doc, participant, now);
123
- if (lease !== undefined)
124
- return { name: participant, state: 'watching', lastAt: cursor?.updatedAt ?? lease.heartbeatAt };
125
- const lastAt = cursor?.updatedAt ?? (snapshot?.joined ? snapshot.lastActiveAt : undefined);
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(({ name: participant }) => {
134
- const snapshot = participantState(state, participant);
135
- const cursor = getReadState(doc, participant);
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 { participants: buildParticipantStatuses(doc, now), now };
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.map((act) => ({ act, index: actStableIndex(act) }));
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((item) => item.index === opts.atIndex);
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(({ index }) => wanted.has(index));
191
+ acts = acts.filter((act) => wanted.has(act.index));
213
192
  }
214
193
  if (opts.afterIndex != null)
215
- acts = acts.filter(({ index }) => index > opts.afterIndex);
194
+ acts = acts.filter((act) => act.index > opts.afterIndex);
216
195
  if (canonicalParticipants.length > 0) {
217
- acts = acts.filter(({ act }) => (act.kind === 'say' && act.reach === 'bell') ||
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(({ act }) => act.at < opts.before);
199
+ acts = acts.filter((act) => act.at < opts.before);
222
200
  if (opts.after != null)
223
- acts = acts.filter(({ act }) => act.at > opts.after);
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(({ act }) => act.kind === 'say' && (act.reach === 'bell' || matchesMentionTarget(act, mention)));
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
- acts = acts.filter(({ act, index }) => {
237
- if (!addressesViewer(act, viewer))
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 output is defined over the same public say/done activities in every
247
- // presentation mode, including --count, --json, and human-readable echo.
248
- acts = acts.filter(({ act }) => act.kind === 'say' || act.kind === 'done');
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(({ act }) => 'body' in act && typeof act.body === 'string' && re.test(act.body));
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.act.at - a.act.at);
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.act.at - b.act.at);
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 = rosterNames(doc).filter((participant) => {
268
- if (!resolveRosterName(doc, participant) || !currentHold(doc.acts))
269
- return false;
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 acts being archived.`);
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: { ...doc.runtime, firstActIndex },
250
+ runtime: doc.runtime,
284
251
  },
285
252
  };
286
253
  }