@astrosheep/square 0.3.70 → 0.3.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activity-feed.d.ts +4 -5
- package/dist/activity-feed.js +14 -10
- package/dist/artifact.d.ts +26 -33
- package/dist/artifact.js +346 -317
- package/dist/attention-presentation.js +2 -9
- package/dist/automatic-session.js +6 -6
- package/dist/boundary-presentation.js +3 -3
- package/dist/catch-decisions.d.ts +19 -12
- package/dist/catch-decisions.js +32 -68
- package/dist/claude-delivery.js +7 -7
- package/dist/claude-mod.js +18 -7
- package/dist/cli/catch-presentation.d.ts +7 -0
- package/dist/cli/catch-presentation.js +55 -0
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/context.js +5 -23
- package/dist/cli/express-presentation.d.ts +10 -0
- package/dist/cli/express-presentation.js +196 -0
- package/dist/cli/maintenance-commands.d.ts +4 -0
- package/dist/cli/maintenance-commands.js +29 -6
- package/dist/cli/observation-commands.d.ts +7 -5
- package/dist/cli/observation-commands.js +275 -262
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +43 -56
- package/dist/cli/registry.js +2 -1
- package/dist/cli/square-commands.d.ts +3 -9
- package/dist/cli/square-commands.js +134 -255
- package/dist/codex-queue.js +4 -1
- package/dist/decisions.d.ts +26 -74
- package/dist/decisions.js +190 -335
- package/dist/delivery-health.js +13 -22
- package/dist/delivery-operations.d.ts +0 -11
- package/dist/delivery-operations.js +44 -33
- package/dist/delivery.d.ts +33 -32
- package/dist/delivery.js +70 -81
- package/dist/help.js +6 -1
- package/dist/inbox.d.ts +27 -0
- package/dist/inbox.js +55 -18
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1 -1
- package/dist/list.d.ts +7 -1
- package/dist/list.js +49 -122
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +7 -37
- package/dist/model.d.ts +4 -44
- package/dist/model.js +1 -3
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.js +13 -7
- package/dist/open-square.d.ts +16 -2
- package/dist/open-square.js +40 -1
- package/dist/perception-projection.d.ts +6 -15
- package/dist/perception-projection.js +6 -26
- package/dist/plugins/opencode/delivery.js +9 -9
- package/dist/plugins/opencode/index.js +3 -3
- package/dist/ports.d.ts +156 -11
- package/dist/presentation-operations.d.ts +1 -1
- package/dist/presentation-operations.js +35 -44
- package/dist/presentation.d.ts +12 -19
- package/dist/presentation.js +33 -54
- package/dist/routes.d.ts +7 -14
- package/dist/routes.js +33 -62
- package/dist/runtime.d.ts +12 -39
- package/dist/runtime.js +33 -164
- package/dist/square-actions.d.ts +11 -21
- package/dist/square-actions.js +171 -271
- package/dist/square-application.d.ts +31 -55
- package/dist/square-application.js +637 -141
- package/dist/square-core.d.ts +3 -94
- package/dist/square-core.js +8 -353
- package/dist/square-domain.d.ts +24 -0
- package/dist/square-domain.js +77 -0
- package/dist/square-facade.d.ts +360 -9
- package/dist/square-migration.d.ts +24 -0
- package/dist/square-migration.js +211 -0
- package/dist/square-projections.d.ts +2 -42
- package/dist/square-projections.js +1 -37
- package/dist/square-sqlite.d.ts +59 -0
- package/dist/square-sqlite.js +702 -0
- package/dist/stream.d.ts +3 -10
- package/dist/stream.js +81 -52
- package/dist/views.d.ts +27 -95
- package/dist/views.js +217 -87
- package/dist/wake-eligibility.d.ts +15 -5
- package/dist/wake-eligibility.js +32 -16
- package/dist/wakes.d.ts +3 -3
- package/dist/wakes.js +31 -27
- package/guides/application-facade-delivery-evidence.md +200 -0
- package/guides/application-facade-design.md +95 -0
- package/guides/change-waits.md +4 -4
- package/guides/migration.md +77 -0
- package/guides/primitive-catch-consolidation.md +85 -0
- package/guides/primitive-history-consolidation.md +76 -0
- package/guides/primitive-status-consolidation.md +87 -0
- package/guides/primitive-storage-consolidation.md +128 -0
- package/guides/row-runtime-delivery-evidence.md +160 -0
- package/guides/row-storage-design.md +567 -0
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +11 -11
- package/plugins/opencode/index.ts +3 -3
- package/plugins/paseo/client/square-message.tsx +70 -52
- package/plugins/paseo/shared/message.ts +15 -0
- package/plugins/pi/square-pi.js +3 -0
- package/dist/activity.d.ts +0 -14
- package/dist/activity.js +0 -248
- package/dist/square-file-adapter.d.ts +0 -26
- package/dist/square-file-adapter.js +0 -85
- package/dist/square-state.d.ts +0 -6
- package/dist/square-state.js +0 -138
- package/dist/square-storage.d.ts +0 -14
- package/dist/square-storage.js +0 -188
- package/dist/square-wiring.d.ts +0 -33
- package/dist/square-wiring.js +0 -109
- package/dist/watch.d.ts +0 -8
- 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,
|
|
4
|
-
import {
|
|
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(
|
|
6
|
+
export function resolveKnownName(facts, name) {
|
|
10
7
|
validateName(name);
|
|
11
|
-
const known =
|
|
12
|
-
if (known === undefined)
|
|
13
|
-
throw new SquareError('invalid_args',
|
|
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(
|
|
13
|
+
export function decideJoin(facts, name, now) {
|
|
18
14
|
validateName(name);
|
|
19
|
-
const
|
|
20
|
-
const joinedName =
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
33
|
-
export function decideImplicitJoin(squareState, name, now) {
|
|
20
|
+
export function decideImplicitJoin(facts, name, now) {
|
|
34
21
|
validateName(name);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
107
|
-
const
|
|
108
|
-
const
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
146
|
-
const notStanding = unmatched.filter((candidate) => resolveNameAt(body, candidate,
|
|
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(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
return { type: '
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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(
|
|
176
|
+
export function coreDone(facts, name, body, now) {
|
|
224
177
|
validateDoneBody(body);
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
239
|
-
const
|
|
182
|
+
function attentionChange(facts, kind, actor, target, now) {
|
|
183
|
+
const current = resolveStanding(facts, actor);
|
|
240
184
|
validateName(target);
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
export function
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
}
|