@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.
Files changed (57) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/codex-plugin/hooks/hooks.json +3 -14
  3. package/dist/activity-feed.js +26 -18
  4. package/dist/activity.js +10 -10
  5. package/dist/artifact.js +138 -203
  6. package/dist/boundary-presentation.js +77 -0
  7. package/dist/claude-hook.js +4 -94
  8. package/dist/cli/context.js +7 -7
  9. package/dist/cli/maintenance-commands.js +10 -26
  10. package/dist/cli/meta-commands.js +3 -6
  11. package/dist/cli/observation-commands.js +48 -53
  12. package/dist/cli/program.js +4 -4
  13. package/dist/cli/registry.js +5 -5
  14. package/dist/cli/square-commands.js +27 -20
  15. package/dist/cmd/notify-once.js +23 -21
  16. package/dist/codex-hook.js +22 -0
  17. package/dist/compact.js +1 -1
  18. package/dist/decisions.js +61 -88
  19. package/dist/delivery-health.js +104 -210
  20. package/dist/delivery.js +68 -18
  21. package/dist/doctor.js +9 -8
  22. package/dist/harness-claude.js +38 -245
  23. package/dist/harness-codex.js +82 -616
  24. package/dist/harness-stage.js +36 -0
  25. package/dist/harness.js +3 -5
  26. package/dist/help.js +43 -35
  27. package/dist/inbox.js +12 -11
  28. package/dist/index.js +10 -121
  29. package/dist/list.js +1 -1
  30. package/dist/model.js +0 -6
  31. package/dist/notification-failures.js +54 -0
  32. package/dist/notifications.js +47 -62
  33. package/dist/paseo-delivery.js +160 -0
  34. package/dist/paseo-state.js +31 -0
  35. package/dist/paseo-timeline.js +58 -188
  36. package/dist/presentation.js +57 -64
  37. package/dist/presented.js +9 -8
  38. package/dist/registry.js +55 -45
  39. package/dist/runtime.js +27 -84
  40. package/dist/square-application.js +135 -130
  41. package/dist/square-core.js +3 -11
  42. package/dist/stream.js +27 -126
  43. package/dist/wake-sink.js +3 -214
  44. package/dist/watch.js +65 -122
  45. package/extensions/square-opencode.js +8 -73
  46. package/extensions/square-pi.js +8 -132
  47. package/guides/architect.md +3 -3
  48. package/guides/participant.md +25 -16
  49. package/package.json +2 -2
  50. package/skills/brainstorm/SKILL.md +25 -32
  51. package/skills/square/.claude-plugin/plugin.json +1 -1
  52. package/skills/square/SKILL.md +39 -107
  53. package/skills/square/hooks/hooks.json +2 -13
  54. package/skills/square-feedback/SKILL.md +4 -4
  55. package/dist/harness-lifecycle.js +0 -102
  56. package/dist/square-store.js +0 -111
  57. 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, validateParticipantName, } from '../model.js';
5
- import { participantCommandPrefix, quoteShell, renderEventCli, renderPublicTail, withJoinNextOutput, withPathOutput, } from '../presentation.js';
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: options, snippet };
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 === '-1' ? 'unlimited' : cap}`, ...throttle, ' · participants (none seeded — first join adds names)'].join('\n'), { participantCount: 0 });
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
- validateParticipantName(intent.name);
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 withJoinNextOutput(context.squarePath, output, { participantCount: inSquareCount(after) });
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 withJoinNextOutput(context.squarePath, `● ${joinedName} is already in the square${fallback}`, { participantCount: inSquareCount(doc) });
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 act options: --beside and --bell are mutually exclusive.');
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("act requires a body argument (a quoted string or '-' with piped stdin)");
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 actCommand = {
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
- forceCommand: `${participantCommandPrefix(context.squarePath, intent.name)} act --force${reachArg} -`,
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].act.actor;
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].act), { participantCount: inSquareCount(doc), held: true });
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].act), { participantCount: inSquareCount(doc) });
209
+ return withPathOutput(context.squarePath, renderEventCli(committed.acts[0]), { participantCount: inSquareCount(doc) });
203
210
  },
204
211
  present: (result) => process.stdout.write(result),
205
212
  };
@@ -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
  });
@@ -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} acts`, ` · kept ${keptCount} acts`, ...(archivedCount > 0 ? [` · sidecar ${archive}`] : [])].join('\n');
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, 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,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', 'act body cannot be empty');
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 act again`);
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 = indexedDelta(doc.acts, readCursor(doc, name));
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.act.kind === 'say') {
65
- const key = item.act.actor.toLocaleLowerCase();
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.act.kind !== 'say' || sameName(item.act.actor, name))
74
+ if (item.kind !== 'say' || sameName(item.actor, name))
69
75
  continue;
70
- const actorKey = item.act.actor.toLocaleLowerCase();
71
- const currentSummary = unreadByParticipant.get(item.act.actor);
72
- unreadByParticipant.set(item.act.actor, {
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.act.at : Math.max(currentSummary.latestAt, item.act.at),
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.act },
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
- 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
- });
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(({ 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 };
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 { 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));
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.map((act) => ({ act, index: actStableIndex(act) }));
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((item) => item.index === opts.atIndex);
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(({ index }) => wanted.has(index));
197
+ acts = acts.filter((act) => wanted.has(act.index));
213
198
  }
214
199
  if (opts.afterIndex != null)
215
- acts = acts.filter(({ index }) => index > opts.afterIndex);
200
+ acts = acts.filter((act) => act.index > opts.afterIndex);
216
201
  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))));
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(({ act }) => act.at < opts.before);
205
+ acts = acts.filter((act) => act.at < opts.before);
222
206
  if (opts.after != null)
223
- acts = acts.filter(({ act }) => act.at > opts.after);
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(({ 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));
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
- 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
- });
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 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');
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(({ act }) => 'body' in act && typeof act.body === 'string' && re.test(act.body));
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.act.at - a.act.at);
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.act.at - b.act.at);
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 = 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
- });
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 acts being archived.`);
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: { ...doc.runtime, firstActIndex },
256
+ runtime: doc.runtime,
284
257
  },
285
258
  };
286
259
  }