@astrosheep/square 0.3.5 → 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 (51) 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 +9 -10
  4. package/dist/artifact.js +126 -202
  5. package/dist/claude-hook.js +45 -21
  6. package/dist/cli/context.js +7 -7
  7. package/dist/cli/maintenance-commands.js +10 -26
  8. package/dist/cli/meta-commands.js +3 -6
  9. package/dist/cli/observation-commands.js +44 -52
  10. package/dist/cli/program.js +4 -4
  11. package/dist/cli/registry.js +5 -5
  12. package/dist/cli/square-commands.js +16 -18
  13. package/dist/cmd/notify-once.js +23 -21
  14. package/dist/compact.js +1 -1
  15. package/dist/decisions.js +53 -86
  16. package/dist/delivery-health.js +104 -210
  17. package/dist/delivery.js +68 -18
  18. package/dist/doctor.js +9 -8
  19. package/dist/harness-claude.js +38 -245
  20. package/dist/harness-codex.js +82 -616
  21. package/dist/harness-stage.js +36 -0
  22. package/dist/harness.js +3 -5
  23. package/dist/help.js +43 -35
  24. package/dist/inbox.js +12 -11
  25. package/dist/index.js +9 -121
  26. package/dist/list.js +1 -1
  27. package/dist/model.js +0 -6
  28. package/dist/notification-failures.js +54 -0
  29. package/dist/notifications.js +47 -62
  30. package/dist/paseo-timeline.js +58 -188
  31. package/dist/presentation.js +55 -63
  32. package/dist/presented.js +9 -8
  33. package/dist/registry.js +55 -45
  34. package/dist/runtime.js +27 -84
  35. package/dist/square-application.js +135 -130
  36. package/dist/square-core.js +3 -11
  37. package/dist/stream.js +27 -126
  38. package/dist/wake-sink.js +134 -188
  39. package/dist/watch.js +65 -122
  40. package/extensions/square-opencode.js +1 -1
  41. package/extensions/square-pi.js +8 -130
  42. package/guides/architect.md +3 -3
  43. package/guides/participant.md +25 -16
  44. package/package.json +2 -2
  45. package/skills/brainstorm/SKILL.md +25 -32
  46. package/skills/square/.claude-plugin/plugin.json +1 -1
  47. package/skills/square/SKILL.md +39 -107
  48. package/skills/square-feedback/SKILL.md +4 -4
  49. package/dist/harness-lifecycle.js +0 -102
  50. package/dist/square-store.js +0 -111
  51. 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),
@@ -135,7 +133,7 @@ function parseActivity(argv, context) {
135
133
  bodyArgs.push(argument);
136
134
  }
137
135
  if (bell && beside !== undefined)
138
- fail('Invalid act options: --beside and --bell are mutually exclusive.');
136
+ fail('Invalid express options: --beside and --bell are mutually exclusive.');
139
137
  const reach = bell ? 'bell' : beside === undefined ? undefined : { beside };
140
138
  if (bodyArgs.length !== 1) {
141
139
  if (bodyArgs.length === 0) {
@@ -143,11 +141,11 @@ function parseActivity(argv, context) {
143
141
  if (piped !== undefined)
144
142
  return { name: requireParticipant(context.name), activity: piped, force, noWait, reach };
145
143
  }
146
- fail("act requires a body argument (a quoted string or '-' with piped stdin)");
144
+ fail("express requires a body argument (a quoted string or '-' with piped stdin)");
147
145
  }
148
146
  return { name: requireParticipant(context.name), activity: bodyArgs[0], force, noWait, reach };
149
147
  }
150
- export const actCommand = {
148
+ export const expressCommand = {
151
149
  parse: parseActivity,
152
150
  async execute(intent, context) {
153
151
  const reachArg = intent.reach === 'bell' ? ' --bell' : intent.reach === undefined ? '' : ` --beside ${quoteShell(intent.reach.beside)}`;
@@ -155,7 +153,7 @@ export const actCommand = {
155
153
  force: intent.force,
156
154
  noWait: intent.noWait,
157
155
  reach: intent.reach,
158
- forceCommand: `${participantCommandPrefix(context.squarePath, intent.name)} act --force${reachArg} -`,
156
+ forceCommand: `${participantCommandPrefix(context.squarePath, intent.name)} express --force${reachArg} -`,
159
157
  });
160
158
  },
161
159
  present: () => { },
@@ -170,7 +168,7 @@ export const doneCommand = {
170
168
  async execute(intent, context) {
171
169
  const body = resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim();
172
170
  const committed = await execute(context.squarePath, { type: 'done', name: intent.name, body, now: nowMs() });
173
- const name = committed.acts[0].act.actor;
171
+ const name = committed.acts[0].actor;
174
172
  recordLocalDone(name, context.squarePath);
175
173
  return withPathOutput(context.squarePath, `× ${name} steps out of the square — done · just now`, { participantCount: inSquareCount(loadSquare(context.squarePath)) });
176
174
  },
@@ -186,7 +184,7 @@ export const holdCommand = {
186
184
  async execute(intent, context) {
187
185
  const committed = await execute(context.squarePath, { type: 'hold', actor: intent.name, body: resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim(), now: nowMs() });
188
186
  const doc = loadSquare(context.squarePath);
189
- return withPathOutput(context.squarePath, renderEventCli(committed.acts[0].act), { participantCount: inSquareCount(doc), held: true });
187
+ return withPathOutput(context.squarePath, renderEventCli(committed.acts[0]), { participantCount: inSquareCount(doc), held: true });
190
188
  },
191
189
  present: (result) => process.stdout.write(result),
192
190
  };
@@ -199,7 +197,7 @@ export const resumeCommand = {
199
197
  async execute(intent, context) {
200
198
  const committed = await execute(context.squarePath, { type: 'resume', actor: intent.name, now: nowMs() });
201
199
  const doc = loadSquare(context.squarePath);
202
- return withPathOutput(context.squarePath, renderEventCli(committed.acts[0].act), { participantCount: inSquareCount(doc) });
200
+ return withPathOutput(context.squarePath, renderEventCli(committed.acts[0]), { participantCount: inSquareCount(doc) });
203
201
  },
204
202
  present: (result) => process.stdout.write(result),
205
203
  };
@@ -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
@@ -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,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
  }