@astrosheep/square 0.3.70 → 0.3.72

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 (126) hide show
  1. package/dist/activity-feed.d.ts +4 -5
  2. package/dist/activity-feed.js +14 -10
  3. package/dist/artifact.d.ts +26 -33
  4. package/dist/artifact.js +346 -317
  5. package/dist/attention-presentation.js +2 -9
  6. package/dist/automatic-session.js +6 -6
  7. package/dist/boundary-presentation.js +3 -3
  8. package/dist/catch-decisions.d.ts +19 -12
  9. package/dist/catch-decisions.js +32 -68
  10. package/dist/claude-delivery.js +7 -7
  11. package/dist/claude-mod.js +18 -7
  12. package/dist/cli/catch-presentation.d.ts +7 -0
  13. package/dist/cli/catch-presentation.js +55 -0
  14. package/dist/cli/context.d.ts +1 -1
  15. package/dist/cli/context.js +5 -23
  16. package/dist/cli/express-presentation.d.ts +10 -0
  17. package/dist/cli/express-presentation.js +196 -0
  18. package/dist/cli/maintenance-commands.d.ts +4 -0
  19. package/dist/cli/maintenance-commands.js +29 -6
  20. package/dist/cli/observation-commands.d.ts +7 -5
  21. package/dist/cli/observation-commands.js +275 -262
  22. package/dist/cli/program.d.ts +1 -1
  23. package/dist/cli/program.js +43 -56
  24. package/dist/cli/registry.js +2 -1
  25. package/dist/cli/square-commands.d.ts +3 -9
  26. package/dist/cli/square-commands.js +134 -255
  27. package/dist/codex-queue.js +4 -1
  28. package/dist/decisions.d.ts +26 -74
  29. package/dist/decisions.js +190 -335
  30. package/dist/delivery-health.js +13 -22
  31. package/dist/delivery-operations.d.ts +0 -11
  32. package/dist/delivery-operations.js +44 -33
  33. package/dist/delivery.d.ts +33 -32
  34. package/dist/delivery.js +70 -81
  35. package/dist/help.js +6 -1
  36. package/dist/inbox.d.ts +27 -0
  37. package/dist/inbox.js +55 -18
  38. package/dist/index.d.ts +2 -4
  39. package/dist/index.js +1 -1
  40. package/dist/list.d.ts +7 -1
  41. package/dist/list.js +49 -122
  42. package/dist/mcp.d.ts +1 -1
  43. package/dist/mcp.js +7 -37
  44. package/dist/model.d.ts +4 -44
  45. package/dist/model.js +1 -3
  46. package/dist/notifications.d.ts +1 -1
  47. package/dist/notifications.js +13 -7
  48. package/dist/open-square.d.ts +16 -2
  49. package/dist/open-square.js +40 -1
  50. package/dist/packages/agent-delivery/src/claude.d.ts +2 -11
  51. package/dist/packages/agent-delivery/src/index.d.ts +2 -42
  52. package/dist/packages/agent-delivery/src/opencode.d.ts +2 -11
  53. package/dist/packages/agent-delivery/src/paseo.d.ts +2 -13
  54. package/dist/packages/agent-delivery/src/pi-client.d.ts +2 -9
  55. package/dist/packages/agent-delivery/src/types.d.ts +42 -0
  56. package/dist/participant-host.d.ts +6 -3
  57. package/dist/participant-host.js +44 -38
  58. package/dist/perception-projection.d.ts +6 -15
  59. package/dist/perception-projection.js +6 -26
  60. package/dist/plugins/opencode/delivery.js +9 -9
  61. package/dist/plugins/opencode/index.js +3 -3
  62. package/dist/ports.d.ts +156 -11
  63. package/dist/presentation-operations.d.ts +1 -1
  64. package/dist/presentation-operations.js +35 -44
  65. package/dist/presentation.d.ts +12 -19
  66. package/dist/presentation.js +33 -54
  67. package/dist/registry.d.ts +2 -0
  68. package/dist/registry.js +1 -1
  69. package/dist/routes.d.ts +7 -14
  70. package/dist/routes.js +34 -72
  71. package/dist/runtime.d.ts +12 -39
  72. package/dist/runtime.js +33 -164
  73. package/dist/square-actions.d.ts +11 -21
  74. package/dist/square-actions.js +171 -275
  75. package/dist/square-application.d.ts +31 -55
  76. package/dist/square-application.js +625 -141
  77. package/dist/square-core.d.ts +3 -94
  78. package/dist/square-core.js +8 -353
  79. package/dist/square-domain.d.ts +24 -0
  80. package/dist/square-domain.js +77 -0
  81. package/dist/square-facade.d.ts +360 -9
  82. package/dist/square-migration.d.ts +24 -0
  83. package/dist/square-migration.js +211 -0
  84. package/dist/square-projections.d.ts +2 -42
  85. package/dist/square-projections.js +1 -37
  86. package/dist/square-sqlite.d.ts +59 -0
  87. package/dist/square-sqlite.js +702 -0
  88. package/dist/stream.d.ts +3 -10
  89. package/dist/stream.js +81 -52
  90. package/dist/views.d.ts +27 -95
  91. package/dist/views.js +217 -87
  92. package/dist/wake-eligibility.d.ts +15 -5
  93. package/dist/wake-eligibility.js +32 -16
  94. package/dist/wakes.d.ts +3 -3
  95. package/dist/wakes.js +31 -27
  96. package/guides/application-facade-delivery-evidence.md +200 -0
  97. package/guides/application-facade-design.md +95 -0
  98. package/guides/change-waits.md +4 -4
  99. package/guides/migration.md +77 -0
  100. package/guides/postcommit-simplification-performance.md +36 -0
  101. package/guides/primitive-catch-consolidation.md +85 -0
  102. package/guides/primitive-history-consolidation.md +76 -0
  103. package/guides/primitive-status-consolidation.md +87 -0
  104. package/guides/primitive-storage-consolidation.md +128 -0
  105. package/guides/row-runtime-delivery-evidence.md +160 -0
  106. package/guides/row-storage-design.md +567 -0
  107. package/package.json +1 -1
  108. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  109. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  110. package/plugins/opencode/delivery.ts +11 -11
  111. package/plugins/opencode/index.ts +3 -3
  112. package/plugins/paseo/client/square-message.tsx +77 -64
  113. package/plugins/paseo/shared/message.ts +16 -0
  114. package/plugins/pi/square-pi.js +5 -1
  115. package/dist/activity.d.ts +0 -14
  116. package/dist/activity.js +0 -248
  117. package/dist/square-file-adapter.d.ts +0 -26
  118. package/dist/square-file-adapter.js +0 -85
  119. package/dist/square-state.d.ts +0 -6
  120. package/dist/square-state.js +0 -138
  121. package/dist/square-storage.d.ts +0 -14
  122. package/dist/square-storage.js +0 -188
  123. package/dist/square-wiring.d.ts +0 -33
  124. package/dist/square-wiring.js +0 -109
  125. package/dist/watch.d.ts +0 -8
  126. package/dist/watch.js +0 -252
package/dist/decisions.js CHANGED
@@ -1,374 +1,229 @@
1
- import { SquareError, sameName, validateName, } from './model.js';
1
+ import { SquareError, isSquareError, sameName, nameKey, validateName, } from './model.js';
2
2
  import { participantIdentity } from './participant-identity.js';
3
- import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs, resolveRosterName, rosterNames, THROTTLE_WINDOW_MS, } from './runtime.js';
4
- import { actDelta, directedPeerSays } from './activity-feed.js';
5
- import { actorOf, formatActivityId, isIgnored, isListening, listeningTo, MAX_IDENTITY_SET_SIZE, validate } from './square-core.js';
6
- import { deriveDeliveryModel } from './delivery.js';
7
- import { compileSearchPattern } from './search.js';
3
+ import { UNREAD_BLOCK_GRACE_MS, freshWatchLease, readCursor, FACT_PAGE_SIZE, THROTTLE_WINDOW_MS } from './runtime.js';
4
+ import { formatActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
8
5
  import { scanMentionCandidates, resolveNameAt } from './mention-parse.js';
9
- export function resolveKnownName(squareState, name) {
6
+ export function resolveKnownName(facts, name) {
10
7
  validateName(name);
11
- const known = resolveRosterName(squareState, name);
12
- if (known === undefined) {
13
- throw new SquareError('invalid_args', `✕ ${participantIdentity(name)} has never stepped into this square`, { reason: 'never_joined' });
14
- }
8
+ const known = facts.participant(name)?.name;
9
+ if (known === undefined)
10
+ throw new SquareError('invalid_args', `${participantIdentity(name)} has never stepped into this square`, { reason: 'never_joined' });
15
11
  return known;
16
12
  }
17
- export function decideJoin(squareState, name, now) {
13
+ export function decideJoin(facts, name, now) {
18
14
  validateName(name);
19
- const knownName = resolveRosterName(squareState, name);
20
- const joinedName = knownName ?? name;
21
- const state = foldedState(squareState);
22
- const result = validate(state, { kind: 'join', actor: joinedName, at: now });
23
- if (!result.ok && result.reason === 'already_joined') {
24
- return { joinedName, addParticipant: false };
25
- }
26
- return {
27
- joinedName,
28
- addParticipant: knownName === undefined,
29
- joinAct: { kind: 'join', actor: joinedName, at: now },
30
- };
15
+ const current = facts.participant(name);
16
+ const joinedName = current?.name ?? name;
17
+ return current?.joined ? { joinedName, addParticipant: false }
18
+ : { joinedName, addParticipant: current === undefined, joinAct: { kind: 'join', actor: joinedName, at: now } };
31
19
  }
32
- /** Automatic presence may enter only once; an explicit join remains the re-entry path. */
33
- export function decideImplicitJoin(squareState, name, now) {
20
+ export function decideImplicitJoin(facts, name, now) {
34
21
  validateName(name);
35
- const knownName = resolveRosterName(squareState, name);
36
- const joinedName = knownName ?? name;
37
- if (knownName !== undefined) {
38
- const current = foldedState(squareState).participants.find((participant) => sameName(participant.name, knownName));
39
- return { joinedName, state: current?.joined === true ? 'active' : 'done' };
40
- }
41
- return {
42
- joinedName,
43
- state: 'joined',
44
- joinAct: { kind: 'join', actor: joinedName, at: now },
45
- };
46
- }
47
- function resolveStandingName(squareState, requestedName) {
48
- validateName(requestedName);
49
- const name = resolveRosterName(squareState, requestedName);
50
- if (name === undefined)
51
- throw new SquareError('not_joined', `${participantIdentity(requestedName)} has not joined this square`);
52
- return name;
53
- }
54
- function requireStanding(squareState, act) {
55
- const result = validate(foldedState(squareState), act);
56
- if (result.ok)
57
- return;
58
- if (result.reason === 'done')
59
- throw new SquareError('already_done', `${act.actor === undefined ? 'participant' : participantIdentity(act.actor)} is already done`);
60
- if (result.reason === 'not_joined')
61
- throw new SquareError('not_joined', `${act.actor === undefined ? 'participant' : participantIdentity(act.actor)} has not joined this square`);
62
- if (result.reason === 'listening_limit')
63
- throw new SquareError('invalid_args', `A participant can listen to at most ${result.limit} others`);
64
- throw new Error(`Unexpected standing validation result: ${result.reason}`);
65
- }
66
- const UNREAD_PREVIEW_LIMIT = 3;
67
- /** The unread activity a viewer has not caught up to, grouped by speaker. */
68
- export function unreadActivitySummaries(squareState, name, now) {
69
- const delivery = deriveDeliveryModel(squareState);
70
- const delta = actDelta(squareState.acts, delivery.cursorFor(name));
71
- const unreadPublic = directedPeerSays(squareState, delta, name, delivery);
72
- const sayCountByActor = new Map();
73
- const unreadByParticipant = new Map();
74
- for (const item of unreadPublic) {
75
- if (item.kind === 'say') {
76
- const key = item.actor.toLocaleLowerCase();
77
- sayCountByActor.set(key, (sayCountByActor.get(key) ?? 0) + 1);
22
+ const current = facts.participant(name);
23
+ return current === undefined ? { joinedName: name, state: 'joined', joinAct: { kind: 'join', actor: name, at: now } }
24
+ : { joinedName: current.name, state: current.joined ? 'active' : 'done' };
25
+ }
26
+ function validateActivityName(name) {
27
+ try {
28
+ validateName(name);
29
+ }
30
+ catch (error) {
31
+ if (isSquareError(error))
32
+ error.facts = { ...error.facts, activityUnsent: true };
33
+ throw error;
34
+ }
35
+ }
36
+ function activityRefusal(code, message, facts) {
37
+ return new SquareError(code, message, { ...facts, activityUnsent: true });
38
+ }
39
+ function resolveStanding(facts, name, activity = false) {
40
+ if (activity)
41
+ validateActivityName(name);
42
+ else
43
+ validateName(name);
44
+ const current = facts.participant(name);
45
+ if (current === undefined) {
46
+ const message = `${participantIdentity(name)} has not joined this square`;
47
+ throw activity ? activityRefusal('not_joined', message) : new SquareError('not_joined', message);
48
+ }
49
+ return current;
50
+ }
51
+ function requireStanding(current) {
52
+ if (current.done)
53
+ throw new SquareError('already_done', `${participantIdentity(current.name)} is already done`);
54
+ if (!current.joined)
55
+ throw new SquareError('not_joined', `${participantIdentity(current.name)} has not joined this square`);
56
+ }
57
+ /** Count the directed suffix, but hydrate only its last three previews per spelling. */
58
+ export function unreadActivitySummaries(facts, name, now) {
59
+ let afterIndex = readCursor(facts, name);
60
+ const numbers = new Map();
61
+ const summaries = new Map();
62
+ while (true) {
63
+ const page = facts.directed(name, { afterIndex }, { edge: 'oldest', limit: FACT_PAGE_SIZE });
64
+ for (const item of page.items) {
65
+ const key = nameKey(item.actor);
66
+ const number = (numbers.get(key) ?? 0) + 1;
67
+ numbers.set(key, number);
68
+ const summary = summaries.get(item.actor) ?? { count: 0, latestAt: -Infinity, previews: [] };
69
+ summary.count++;
70
+ summary.latestAt = Math.max(summary.latestAt, item.at);
71
+ summary.previews.push({ index: item.index, number });
72
+ if (summary.previews.length > 3)
73
+ summary.previews.shift();
74
+ summaries.set(item.actor, summary);
78
75
  }
79
- if (sameName(item.actor, name))
80
- continue;
81
- const actorKey = item.actor.toLocaleLowerCase();
82
- const currentSummary = unreadByParticipant.get(item.actor);
83
- unreadByParticipant.set(item.actor, {
84
- count: (currentSummary?.count ?? 0) + 1,
85
- latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
86
- previews: [
87
- ...(currentSummary?.previews ?? []),
88
- { number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: delivery.perceive(item, name) },
89
- ].slice(-UNREAD_PREVIEW_LIMIT),
90
- });
91
- }
92
- return [...unreadByParticipant.entries()]
93
- .map(([participant, summary]) => ({
94
- name: participant,
95
- count: summary.count,
96
- latestActivityAgeMs: Math.max(0, now - summary.latestAt),
97
- previews: summary.previews,
98
- }))
99
- .sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
100
- }
101
- export function decideAct(squareState, input) {
102
- const { now, force } = input;
103
- const name = resolveStandingName(squareState, input.name);
104
- const body = input.body;
76
+ if (!page.hasMore)
77
+ break;
78
+ afterIndex = page.items.at(-1).index;
79
+ }
80
+ const selected = facts.activities([...summaries.values()].flatMap((item) => item.previews.map((preview) => preview.index)));
81
+ const byIndex = new Map(selected.map((item) => [item.index, item]));
82
+ return [...summaries].map(([name, summary]) => ({
83
+ name, count: summary.count, latestActivityAgeMs: Math.max(0, now - summary.latestAt),
84
+ previews: summary.previews.map((preview) => ({ number: preview.number, act: byIndex.get(preview.index), perception: 'full' })),
85
+ })).sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
86
+ }
87
+ export function decideAct(facts, input) {
88
+ const { now, force, body, reach, reply } = input;
89
+ const current = resolveStanding(facts, input.name, true);
90
+ const name = current.name;
105
91
  if (body.trim() === '')
106
- throw new SquareError('invalid_args', 'express body cannot be empty');
107
- const reach = input.reach;
108
- const state = foldedState(squareState);
109
- const requestedMentions = input.mentions ?? [];
92
+ throw activityRefusal('invalid_args', 'express body cannot be empty');
93
+ const roster = facts.roster();
94
+ const joinedNames = roster.filter((participant) => participant.joined).map((participant) => participant.name);
110
95
  const mentionNames = [];
111
- const joinedNames = state.participants.filter((participant) => participant.joined).map((participant) => participant.name);
112
- for (const requested of requestedMentions) {
113
- validateName(requested);
96
+ const addMention = (resolved) => {
97
+ if (mentionNames.some((existing) => sameName(existing, resolved)))
98
+ return;
99
+ if (mentionNames.length >= MAX_IDENTITY_SET_SIZE)
100
+ throw activityRefusal('invalid_args', `An activity can mention at most ${MAX_IDENTITY_SET_SIZE} participants`);
101
+ mentionNames.push(resolved);
102
+ };
103
+ for (const requested of input.mentions ?? []) {
104
+ validateActivityName(requested);
114
105
  const resolved = joinedNames.find((candidate) => sameName(candidate, requested));
115
- if (resolved === undefined) {
116
- throw new SquareError('invalid_args', `✕ ${participantIdentity(requested)} is not standing in this square`, { reason: 'not_standing' });
117
- }
118
- if (!mentionNames.some((existing) => sameName(existing, resolved))) {
119
- if (mentionNames.length >= MAX_IDENTITY_SET_SIZE) {
120
- throw new SquareError('invalid_args', `An activity can mention at most ${MAX_IDENTITY_SET_SIZE} participants`);
121
- }
122
- mentionNames.push(resolved);
123
- }
106
+ if (resolved === undefined)
107
+ throw activityRefusal('invalid_args', `${participantIdentity(requested)} is not standing in this square`, { reason: 'not_standing', targets: [requested] });
108
+ addMention(resolved);
124
109
  }
125
110
  if (input.noMention !== true) {
126
- // Body @name tokens resolve against the standing roster at landing time; the stored
127
- // body never changes, only mention metadata is derived. Explicit --no-mention turns
128
- // this scan off entirely.
129
111
  const unmatched = [];
130
112
  for (const candidate of scanMentionCandidates(body)) {
131
113
  const resolved = resolveNameAt(body, candidate, joinedNames);
132
- if (resolved === undefined) {
114
+ if (resolved === undefined)
133
115
  unmatched.push(candidate);
134
- continue;
135
- }
136
- if (!mentionNames.some((existing) => sameName(existing, resolved))) {
137
- if (mentionNames.length >= MAX_IDENTITY_SET_SIZE) {
138
- throw new SquareError('invalid_args', `An activity can mention at most ${MAX_IDENTITY_SET_SIZE} participants`);
139
- }
140
- mentionNames.push(resolved);
141
- }
116
+ else
117
+ addMention(resolved);
142
118
  }
143
- // A bell already reaches everyone: only there does an unmatched @token stay literal text.
144
119
  if (reach !== 'bell' && unmatched.length > 0) {
145
- const roster = rosterNames(squareState);
146
- const notStanding = unmatched.filter((candidate) => resolveNameAt(body, candidate, roster) !== undefined);
120
+ const names = roster.map((participant) => participant.name);
121
+ const notStanding = unmatched.filter((candidate) => resolveNameAt(body, candidate, names) !== undefined);
147
122
  const unknown = unmatched.filter((candidate) => !notStanding.includes(candidate));
148
123
  const lines = [];
149
- if (unknown.length > 0) {
150
- lines.push(`✕ ${unknown.map((candidate) => participantIdentity(candidate.token)).join(', ')} ${unknown.length === 1 ? 'does' : 'do'} not match anyone in this square`);
151
- }
152
- if (notStanding.length > 0) {
153
- lines.push(`✕ ${notStanding.map((candidate) => participantIdentity(candidate.token)).join(', ')} ${notStanding.length === 1 ? 'is' : 'are'} not standing in this square`);
154
- }
155
- lines.push(' · wrap literal @text in backticks, or pass --no-mention to land bare');
156
- throw new SquareError('invalid_args', lines.join('\n'), { reason: 'unmatched_mention' });
124
+ if (unknown.length > 0)
125
+ lines.push(`${unknown.map((candidate) => participantIdentity(candidate.token)).join(', ')} ${unknown.length === 1 ? 'does' : 'do'} not match anyone in this square`);
126
+ if (notStanding.length > 0)
127
+ lines.push(`${notStanding.map((candidate) => participantIdentity(candidate.token)).join(', ')} ${notStanding.length === 1 ? 'is' : 'are'} not standing in this square`);
128
+ throw activityRefusal('invalid_args', lines.join('\n'), { reason: 'unmatched_mention', targets: unmatched.map((candidate) => candidate.token) });
157
129
  }
158
130
  }
159
- if (reach === 'bell' && mentionNames.length > 0) {
160
- throw new SquareError('invalid_args', '✕ a bell cannot be combined with a mention — remove --mention or the @name in the body.', { reason: 'bell_mention_conflict' });
161
- }
162
- const reply = input.reply;
163
- if (reply !== undefined) {
164
- if (!Number.isSafeInteger(reply) || reply < 0 || reply >= squareState.runtime.nextActIndex) {
165
- const label = Number.isSafeInteger(reply) && reply >= 0 ? formatActivityId(reply) : String(reply);
166
- throw new SquareError('invalid_args', `Unknown reply activity: ${label}`);
167
- }
168
- }
169
- // A non-standing author never rejects the activity: the reply still lands and the
170
- // speaker is told the notification did not reach them.
171
- const repliedTo = reply === undefined ? undefined : squareState.acts.find((act) => act.index === reply);
172
- const repliedToAuthor = repliedTo === undefined ? undefined : actorOf(repliedTo);
173
- const replyAuthorNotHere = repliedToAuthor === undefined || sameName(repliedToAuthor, name)
174
- || state.participants.some((participant) => participant.joined && sameName(participant.name, repliedToAuthor))
175
- ? undefined
176
- : repliedToAuthor;
177
- const result = validate(state, {
178
- kind: 'say', actor: name, at: now, body, mentions: mentionNames,
179
- ...(reach !== undefined ? { reach } : {}),
180
- ...(reply !== undefined ? { reply } : {}),
181
- }, { hardCap: squareState.hardCap, throttlePerMinute: squareState.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
182
- if (!result.ok) {
183
- if (result.reason === 'done')
184
- throw new SquareError('already_done', `${name} is already done`);
185
- if (result.reason === 'mention_limit')
186
- throw new SquareError('invalid_args', `An activity can mention at most ${result.limit} participants`);
187
- if (result.reason === 'held')
188
- return { type: 'held', reason: result.hold.reason };
189
- if (result.reason === 'hard_cap')
190
- return { type: 'capped', count: result.count, hardCap: result.hardCap };
191
- if (result.reason === 'throttled')
192
- return { type: 'throttled', delayMs: result.delayMs };
193
- if (result.reason === 'bell_quota')
194
- return { type: 'bell_quota', nextAt: result.nextAt };
195
- if (result.reason === 'not_joined')
196
- throw new SquareError('not_joined', `${name} has not joined this square`);
197
- }
198
- const delivery = deriveDeliveryModel(squareState);
199
- const delta = actDelta(squareState.acts, delivery.cursorFor(name));
200
- const unreadPublic = directedPeerSays(squareState, delta, name, delivery);
201
- const activitySummaries = unreadActivitySummaries(squareState, name, now);
202
- const latestActivityAgeMs = activitySummaries[0]?.latestActivityAgeMs;
203
- const hasUnread = unreadPublic.length > 0;
204
- const hasFreshUnreadActivity = latestActivityAgeMs !== undefined && latestActivityAgeMs <= UNREAD_BLOCK_GRACE_MS;
205
- if (!force && hasUnread && !hasFreshUnreadActivity) {
206
- return { type: 'blocked', activitySummaries };
207
- }
208
- return {
209
- type: 'sent',
210
- act: {
211
- kind: 'say', actor: name, at: now, body, mentions: mentionNames,
212
- ...(reach !== undefined ? { reach } : {}),
213
- ...(reply !== undefined ? { reply } : {}),
214
- },
215
- ...(replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere }),
216
- };
131
+ if (reach === 'bell' && mentionNames.length > 0)
132
+ throw activityRefusal('invalid_args', 'A bell cannot be combined with a mention', { reason: 'bell_mention_conflict', targets: mentionNames });
133
+ if (input.requireExplicitReach && reach !== 'bell' && mentionNames.length === 0 && reply === undefined && input.noMention !== true) {
134
+ throw activityRefusal('invalid_args', 'Express needs a recipient, reply, bell, or explicit bare reach', { reason: 'explicit_reach_required' });
135
+ }
136
+ const header = facts.header();
137
+ if (reply !== undefined && (!Number.isSafeInteger(reply) || reply < 0 || reply >= header.nextActIndex)) {
138
+ const label = Number.isSafeInteger(reply) && reply >= 0 ? formatActivityId(reply) : String(reply);
139
+ throw activityRefusal('invalid_args', `Unknown reply activity: ${label}`, { reason: 'invalid_reply', activityId: label, activityExists: false });
140
+ }
141
+ const replyAuthor = reply === undefined ? undefined : facts.activity(reply)?.actor;
142
+ const replyAuthorNotHere = replyAuthor === undefined || sameName(replyAuthor, name) || facts.participant(replyAuthor)?.joined ? undefined : replyAuthor;
143
+ // Preserve admission priority after metadata validation.
144
+ if (current.done)
145
+ throw activityRefusal('already_done', `${name} is already done`);
146
+ if (!current.joined)
147
+ throw activityRefusal('not_joined', `${name} has not joined this square`);
148
+ const hold = facts.hold();
149
+ if (hold.active)
150
+ return { type: 'held', reason: hold.reason };
151
+ if (header.hardCap !== null && current.sayCount >= header.hardCap)
152
+ return { type: 'capped', count: current.sayCount, hardCap: header.hardCap };
153
+ if (reach === 'bell') {
154
+ const recent = facts.recentSayTimes(now - 3600000, 1, name, true);
155
+ if (recent.length > 0)
156
+ return { type: 'bell_quota', nextAt: recent[0] + 3600000 };
157
+ }
158
+ if (header.throttlePerMinute !== undefined) {
159
+ const recent = facts.recentSayTimes(now - THROTTLE_WINDOW_MS, header.throttlePerMinute);
160
+ if (recent.length >= header.throttlePerMinute)
161
+ return { type: 'throttled', delayMs: Math.max(1, recent[0] + THROTTLE_WINDOW_MS - now) };
162
+ }
163
+ if (!force) {
164
+ const activitySummaries = unreadActivitySummaries(facts, name, now);
165
+ if (activitySummaries.length > 0 && activitySummaries[0].latestActivityAgeMs > UNREAD_BLOCK_GRACE_MS)
166
+ return { type: 'blocked', activitySummaries };
167
+ }
168
+ return { type: 'sent', act: { kind: 'say', actor: name, at: now, body, mentions: mentionNames,
169
+ ...(reach === undefined ? {} : { reach }), ...(reply === undefined ? {} : { reply }) },
170
+ ...(replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere }) };
217
171
  }
218
172
  export function validateDoneBody(body) {
219
- if (body !== undefined && body !== '') {
220
- throw new SquareError('invalid_args', 'done only leaves the square and does not accept a message; use express before leaving');
221
- }
173
+ if (body !== undefined && body !== '')
174
+ throw new SquareError('invalid_args', 'done only leaves the square and does not accept a message; use express before leaving', { reason: 'invalid_done_body' });
222
175
  }
223
- export function coreDone(squareState, name, body, now) {
176
+ export function coreDone(facts, name, body, now) {
224
177
  validateDoneBody(body);
225
- const resolvedName = resolveStandingName(squareState, name);
226
- const act = { kind: 'done', actor: resolvedName, at: now };
227
- requireStanding(squareState, act);
228
- return act;
229
- }
230
- export function coreListen(squareState, actor, target, now) {
231
- const resolvedActor = resolveStandingName(squareState, actor);
232
- validateName(target);
233
- const state = foldedState(squareState);
234
- const act = { kind: 'listen', actor: resolvedActor, target, at: now };
235
- requireStanding(squareState, act);
236
- return isListening(state, resolvedActor, target) && !isIgnored(state, resolvedActor, target) ? undefined : act;
178
+ const current = resolveStanding(facts, name);
179
+ requireStanding(current);
180
+ return { kind: 'done', actor: current.name, at: now };
237
181
  }
238
- export function coreIgnore(squareState, actor, target, now) {
239
- const resolvedActor = resolveStandingName(squareState, actor);
182
+ function attentionChange(facts, kind, actor, target, now) {
183
+ const current = resolveStanding(facts, actor);
240
184
  validateName(target);
241
- const state = foldedState(squareState);
242
- const act = { kind: 'ignore', actor: resolvedActor, target, at: now };
243
- requireStanding(squareState, act);
244
- return isIgnored(state, resolvedActor, target) ? undefined : act;
245
- }
246
- export function coreListening(squareState, actor) {
247
- const resolvedActor = resolveStandingName(squareState, actor);
248
- return listeningTo(foldedState(squareState), resolvedActor);
249
- }
250
- export function coreHold(squareState, actor, body, now) {
251
- const resolvedName = resolveStandingName(squareState, actor);
252
- const act = { kind: 'hold', actor: resolvedName, at: now, body: body.replace(/\r\n/g, '\n').trim() };
253
- requireStanding(squareState, act);
254
- return act;
255
- }
256
- export function coreResume(squareState, actor, now) {
257
- const resolvedName = resolveStandingName(squareState, actor);
258
- const act = { kind: 'resume', actor: resolvedName, at: now };
259
- requireStanding(squareState, act);
260
- return act;
185
+ requireStanding(current);
186
+ const outgoing = facts.attention(current.name);
187
+ const existing = outgoing.find((item) => sameName(item.target, target));
188
+ if (kind === 'listen' && existing?.kind !== 'listen' && outgoing.filter((item) => item.kind === 'listen').length >= MAX_IDENTITY_SET_SIZE) {
189
+ throw new SquareError('invalid_args', `A participant can listen to at most ${MAX_IDENTITY_SET_SIZE} others`);
190
+ }
191
+ return existing?.kind === kind ? undefined : { kind, actor: current.name, target, at: now };
192
+ }
193
+ export function coreListen(facts, actor, target, now) { return attentionChange(facts, 'listen', actor, target, now); }
194
+ export function coreIgnore(facts, actor, target, now) { return attentionChange(facts, 'ignore', actor, target, now); }
195
+ export function coreListening(facts, actor) {
196
+ const current = resolveStanding(facts, actor);
197
+ return facts.attention(current.name).filter((item) => item.kind === 'listen').map((item) => item.target);
198
+ }
199
+ export function coreHold(facts, actor, body, now) {
200
+ const current = resolveStanding(facts, actor);
201
+ requireStanding(current);
202
+ return { kind: 'hold', actor: current.name, at: now, body: body.replace(/\r\n/g, '\n').trim() };
203
+ }
204
+ export function coreResume(facts, actor, now) {
205
+ const current = resolveStanding(facts, actor);
206
+ requireStanding(current);
207
+ return { kind: 'resume', actor: current.name, at: now };
261
208
  }
262
209
  export function compareParticipantActivity(a, b) {
263
210
  return (b.lastActiveAt ?? -Infinity) - (a.lastActiveAt ?? -Infinity) || a.name.localeCompare(b.name);
264
211
  }
265
- function buildParticipantStatuses(squareState, now, state = foldedState(squareState), suppliedDelivery) {
266
- const delivery = suppliedDelivery ?? deriveDeliveryModel(squareState);
267
- return state.participants.map((snapshot) => {
268
- const participant = snapshot.name;
269
- let lastActiveAt = snapshot.lastActiveAt;
270
- // Receipt time is evidence of catching; the consumed activity's time is not.
271
- if (snapshot.joined) {
272
- for (const observation of Object.values(squareState.runtime.observations[participant] ?? {})) {
273
- if (observation.state === 'seen')
274
- lastActiveAt = Math.max(lastActiveAt ?? -Infinity, observation.at);
275
- }
276
- }
277
- const presence = snapshot.done ? 'done' : !snapshot.joined ? 'never-joined'
278
- : freshWatchLease(squareState, participant, now) !== undefined ? 'watching' : 'active';
279
- const participantStatus = snapshot?.done ? 'done' : snapshot?.joined ? 'active' : 'not joined';
280
- const consumedThrough = delivery.cursorFor(participant);
281
- let unreadActivityCount = 0;
282
- if (snapshot?.joined) {
283
- for (const act of squareState.acts) {
284
- if (actStableIndex(act) <= consumedThrough)
285
- continue;
286
- if (directedPeerSays(squareState, [act], participant, delivery).length > 0)
287
- unreadActivityCount++;
288
- }
212
+ export function coreParticipants(facts, now) {
213
+ return facts.roster().map((current) => {
214
+ let lastActiveAt = current.lastActiveAt;
215
+ if (current.joined) {
216
+ const seenAt = facts.receiptFacts(current.name, current.lastJoinIndex ?? -1).latestAt;
217
+ if (seenAt !== undefined)
218
+ lastActiveAt = Math.max(lastActiveAt ?? -Infinity, seenAt);
289
219
  }
290
220
  return {
291
- name: participant,
292
- state: participantStatus,
293
- listening: listeningTo(state, participant),
294
- activityCount: snapshot?.activityCount ?? 0,
295
- lastActiveAt,
296
- presence,
297
- unreadActivityCount,
298
- pendingMentionCount: snapshot?.joined ? delivery.pendingFor(participant).length : 0,
221
+ name: current.name, state: current.done ? 'done' : current.joined ? 'active' : 'not joined',
222
+ listening: facts.attention(current.name).filter((item) => item.kind === 'listen').map((item) => item.target),
223
+ activityCount: current.sayCount, lastActiveAt,
224
+ presence: current.done ? 'done' : !current.joined ? 'never-joined' : freshWatchLease(facts, current.name, now) !== undefined ? 'watching' : 'active',
225
+ unreadActivityCount: current.joined ? facts.directedCount(current.name, { afterIndex: readCursor(facts, current.name) }) : 0,
226
+ pendingMentionCount: current.joined ? facts.directedCount(current.name, { afterIndex: current.lastJoinIndex ?? -1, unseenOnly: true }) : 0,
299
227
  };
300
228
  }).sort(compareParticipantActivity);
301
229
  }
302
- export function coreStatus(squareState, now, delivery) {
303
- const state = foldedState(squareState);
304
- const latestAct = publicActs(squareState.acts).at(-1);
305
- return {
306
- hardCap: squareState.hardCap,
307
- throttlePerMinute: squareState.throttlePerMinute,
308
- activeCount: state.joined.length,
309
- doneCount: state.done.length,
310
- holdActive: state.hold.active,
311
- holdReason: state.hold.reason,
312
- holdActor: state.hold.actor,
313
- holdAt: state.hold.at,
314
- participants: buildParticipantStatuses(squareState, now, state, delivery),
315
- latestAct,
316
- now,
317
- };
318
- }
319
- export function coreParticipants(squareState, now, delivery) {
320
- return buildParticipantStatuses(squareState, now, undefined, delivery);
321
- }
322
- export function coreActivities(squareState, opts, suppliedDelivery) {
323
- const participants = opts.participants ?? [];
324
- const canonicalParticipants = participants.map((participant) => resolveKnownName(squareState, participant));
325
- let acts = [...squareState.acts];
326
- let delivery = suppliedDelivery;
327
- const projected = () => delivery ??= deriveDeliveryModel(squareState);
328
- // --at establishes one or more context windows first; other filters AND inside their union.
329
- if (opts.atIndexes !== undefined && opts.atIndexes.length > 0) {
330
- const before = opts.beforeContext ?? 0;
331
- const after = opts.afterContext ?? 0;
332
- const selected = new Set();
333
- for (const atIndex of opts.atIndexes) {
334
- const centerPos = acts.findIndex((act) => act.index === atIndex);
335
- if (centerPos < 0)
336
- continue;
337
- for (const act of acts.slice(Math.max(0, centerPos - before), centerPos + after + 1))
338
- selected.add(act.index);
339
- }
340
- acts = acts.filter((act) => selected.has(act.index));
341
- }
342
- if (opts.afterIndex != null)
343
- acts = acts.filter((act) => act.index > opts.afterIndex);
344
- if (opts.beforeIndex != null)
345
- acts = acts.filter((act) => act.index < opts.beforeIndex);
346
- if (canonicalParticipants.length > 0) {
347
- acts = acts.filter((act) => act.actor !== undefined && canonicalParticipants.some((participant) => sameName(participant, act.actor)));
348
- }
349
- if (opts.after != null)
350
- acts = acts.filter((act) => act.at > opts.after);
351
- if (opts.mention != null) {
352
- const mention = resolveKnownName(squareState, opts.mention);
353
- acts = acts.filter((act) => act.kind === 'say' && projected().plan(act).some((item) => sameName(item.recipient, mention)));
354
- }
355
- const search = opts.grep !== undefined ? { pattern: opts.grep, fixed: false } : opts.fixed !== undefined ? { pattern: opts.fixed, fixed: true } : undefined;
356
- if (search !== undefined && search.pattern !== '') {
357
- // Search only the public activity model rendered by history, but include all
358
- // of its user-facing fields rather than coupling matching to rendered text.
359
- acts = acts.filter((act) => act.kind === 'say' || act.kind === 'done');
360
- const re = compileSearchPattern(search.pattern, search.fixed);
361
- acts = acts.filter((act) => [
362
- actId(act.index),
363
- act.actor ?? '',
364
- 'body' in act && typeof act.body === 'string' ? act.body : '',
365
- ].some((field) => re.test(field)));
366
- }
367
- if (opts.order === 'desc') {
368
- acts = [...acts].sort((a, b) => b.index - a.index || b.at - a.at);
369
- }
370
- else {
371
- acts = [...acts].sort((a, b) => a.index - b.index || a.at - b.at);
372
- }
373
- return acts;
374
- }