@astrosheep/square 0.3.12 → 0.3.13

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 (121) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  2. package/codex-plugin/hooks/hooks.json +9 -0
  3. package/dist/activity-feed.d.ts +14 -0
  4. package/dist/activity-feed.js +5 -5
  5. package/dist/activity.d.ts +9 -0
  6. package/dist/activity.js +111 -90
  7. package/dist/artifact.d.ts +20 -0
  8. package/dist/artifact.js +10 -29
  9. package/dist/automatic-session.d.ts +5 -0
  10. package/dist/automatic-session.js +86 -0
  11. package/dist/boundary-presentation.d.ts +5 -0
  12. package/dist/boundary-presentation.js +5 -4
  13. package/dist/claude-hook.d.ts +9 -0
  14. package/dist/claude-hook.js +35 -3
  15. package/dist/cli/context.d.ts +35 -0
  16. package/dist/cli/context.js +25 -30
  17. package/dist/cli/harness-command.d.ts +28 -0
  18. package/dist/cli/harness-command.js +29 -14
  19. package/dist/cli/maintenance-commands.d.ts +7 -0
  20. package/dist/cli/maintenance-commands.js +3 -3
  21. package/dist/cli/meta-commands.d.ts +5 -0
  22. package/dist/cli/observation-commands.d.ts +20 -0
  23. package/dist/cli/observation-commands.js +155 -128
  24. package/dist/cli/program.d.ts +2 -0
  25. package/dist/cli/program.js +2 -12
  26. package/dist/cli/registry.d.ts +9 -0
  27. package/dist/cli/registry.js +1 -2
  28. package/dist/cli/square-commands.d.ts +34 -0
  29. package/dist/cli/square-commands.js +81 -74
  30. package/dist/cmd/notify-once.d.ts +2 -0
  31. package/dist/codex-hook.d.ts +10 -0
  32. package/dist/codex-hook.js +35 -3
  33. package/dist/decisions.d.ts +92 -0
  34. package/dist/decisions.js +83 -77
  35. package/dist/delivery-health.d.ts +21 -0
  36. package/dist/delivery-health.js +40 -34
  37. package/dist/delivery.d.ts +81 -0
  38. package/dist/delivery.js +60 -22
  39. package/dist/file-lock.d.ts +6 -0
  40. package/dist/harness-claude.d.ts +19 -0
  41. package/dist/harness-claude.js +8 -3
  42. package/dist/harness-codex.d.ts +29 -0
  43. package/dist/harness-codex.js +2 -2
  44. package/dist/harness-links.d.ts +17 -0
  45. package/dist/harness-links.js +2 -2
  46. package/dist/harness-pi.d.ts +11 -0
  47. package/dist/harness-pi.js +2 -2
  48. package/dist/harness-stage.d.ts +6 -0
  49. package/dist/harness.d.ts +20 -0
  50. package/dist/harness.js +5 -3
  51. package/dist/help.d.ts +6 -0
  52. package/dist/help.js +6 -5
  53. package/dist/identity.d.ts +13 -0
  54. package/dist/inbox.d.ts +2 -0
  55. package/dist/inbox.js +15 -19
  56. package/dist/index.d.ts +13 -0
  57. package/dist/index.js +31 -69
  58. package/dist/landing.d.ts +10 -0
  59. package/dist/landing.js +95 -0
  60. package/dist/list.d.ts +1 -0
  61. package/dist/list.js +40 -18
  62. package/dist/model.d.ts +147 -0
  63. package/dist/model.js +12 -2
  64. package/dist/notifications.d.ts +30 -0
  65. package/dist/notifications.js +75 -91
  66. package/dist/open-square.d.ts +11 -0
  67. package/dist/open-square.js +4 -0
  68. package/dist/participant-identity.d.ts +1 -0
  69. package/dist/participant-identity.js +3 -0
  70. package/dist/paseo-connection.d.ts +15 -0
  71. package/dist/paseo-delivery.d.ts +14 -0
  72. package/dist/paseo-state.d.ts +11 -0
  73. package/dist/paseo-timeline.d.ts +16 -0
  74. package/dist/presence.d.ts +3 -0
  75. package/dist/presence.js +42 -0
  76. package/dist/presentation.d.ts +91 -0
  77. package/dist/presentation.js +33 -29
  78. package/dist/presented.d.ts +11 -0
  79. package/dist/registry.d.ts +58 -0
  80. package/dist/registry.js +36 -3
  81. package/dist/routes.d.ts +42 -0
  82. package/dist/runtime.d.ts +45 -0
  83. package/dist/runtime.js +42 -34
  84. package/dist/search.d.ts +11 -0
  85. package/dist/square-core.d.ts +111 -0
  86. package/dist/square-core.js +7 -0
  87. package/dist/square-facade.d.ts +86 -0
  88. package/dist/square-facade.js +1 -0
  89. package/dist/square-file-adapter.d.ts +18 -0
  90. package/dist/square-file-adapter.js +70 -0
  91. package/dist/square-storage.d.ts +20 -0
  92. package/dist/square-storage.js +171 -0
  93. package/dist/square-wiring.d.ts +19 -0
  94. package/dist/square-wiring.js +70 -0
  95. package/dist/square.d.ts +2 -0
  96. package/dist/state-cell.d.ts +14 -0
  97. package/dist/state-cell.js +1 -0
  98. package/dist/stream.d.ts +3 -0
  99. package/dist/stream.js +19 -23
  100. package/dist/time.d.ts +6 -0
  101. package/dist/views.d.ts +95 -0
  102. package/dist/views.js +81 -0
  103. package/dist/wake-attempts.d.ts +39 -0
  104. package/dist/wake-evidence.d.ts +12 -0
  105. package/dist/wake-evidence.js +7 -10
  106. package/dist/wake-port.d.ts +20 -0
  107. package/dist/wake-sink.d.ts +12 -0
  108. package/dist/wakes.d.ts +40 -0
  109. package/dist/wakes.js +62 -0
  110. package/dist/watch.d.ts +2 -0
  111. package/dist/watch.js +89 -210
  112. package/extensions/square-opencode.js +27 -1
  113. package/extensions/square-pi.js +13 -0
  114. package/package.json +5 -2
  115. package/skills/brainstorm/SKILL.md +26 -26
  116. package/skills/square/.claude-plugin/plugin.json +1 -1
  117. package/skills/square/SKILL.md +47 -29
  118. package/skills/square/hooks/hooks.json +9 -0
  119. package/skills/square-feedback/SKILL.md +2 -2
  120. package/dist/compact.js +0 -26
  121. package/dist/square-application.js +0 -193
@@ -1,13 +1,13 @@
1
1
  import { cmdActivity } from '../activity.js';
2
- import { loadSquare } from '../artifact.js';
3
- import { cmdCompact } from '../compact.js';
4
- import { SquareError, formatHardCap, validateName, } from '../model.js';
5
- import { participantCommandPrefix, renderEventCli, renderPublicTail, withPathOutput, } from '../presentation.js';
6
- import { hasAutomaticDeliveryIdentity, localParticipantOwner, recordLocalDone, recordLocalJoin } from '../registry.js';
7
- import { sweepPendingNotifications } from '../notifications.js';
8
- import { inSquareCount, isCurrentlyJoined, nowMs, resolveRosterName } from '../runtime.js';
9
- import { createSquare, execute } from '../square-application.js';
10
- import { formatActivityId, parseActivityId } from '../square-core.js';
2
+ import { formatActivityId, formatHardCap, parseActivityId, } from '../model.js';
3
+ import { participantCommandPrefix, participantIdentity, renderEventCli, renderAmbientEvent, withPathOutput, } from '../presentation.js';
4
+ import { hasAutomaticDeliveryIdentity, localParticipantOwner, recordLocalDone, recordLocalJoin, } from '../registry.js';
5
+ import { sweepPendingNotifications, wakeNotifierForSquare } from '../notifications.js';
6
+ import { nowMs } from '../runtime.js';
7
+ import { createSquare, openSquare } from '../square-file-adapter.js';
8
+ import { closeOpenSquare } from '../open-square.js';
9
+ import { Square } from '../square-wiring.js';
10
+ import { entryPresentation, eventPresentation } from '../views.js';
11
11
  import { fail, parseHardCap, parsePositiveInteger, readPipedBodyFallback, readStdinSync, requireParticipant, requireValue, resolveBody, usage, } from './context.js';
12
12
  function parseBuild(argv) {
13
13
  const options = { force: false, hardCap: null };
@@ -79,57 +79,55 @@ function parseJoin(argv, context) {
79
79
  export const joinCommand = {
80
80
  parse: parseJoin,
81
81
  async execute(intent, context) {
82
- validateName(intent.name);
82
+ const beforeSquare = await openSquare(context.squarePath, { clock: nowMs });
83
+ const before = await entryPresentation(beforeSquare, intent.name, intent.lastN);
84
+ await closeOpenSquare(beforeSquare);
85
+ const square = await Square.at({ path: context.squarePath, clock: nowMs, notifier: wakeNotifierForSquare(context.squarePath) });
83
86
  try {
84
- const committed = await execute(context.squarePath, { type: 'join', name: intent.name, now: nowMs() });
85
- const joinedName = committed.result.joinedName;
86
- const isRejoin = !committed.result.addParticipant;
87
- const after = loadSquare(context.squarePath);
88
- const preamble = after.preamble.at(-1) === '---' ? after.preamble.slice(0, -1) : after.preamble;
87
+ const participant = await square.join(intent.name);
88
+ const joinedName = participant.name;
89
+ const isRejoin = before.joined;
90
+ const reconnect = isRejoin
91
+ && localParticipantOwner(context.squarePath, joinedName) !== undefined;
92
+ if (isRejoin && !intent.kick && !reconnect) {
93
+ fail([
94
+ `✕ ${participantIdentity(joinedName)} shoos you out of the square`,
95
+ ` · a same-named participant stands here — the name is taken`,
96
+ ` · --kick banishes her and the name becomes yours`,
97
+ `» ${participantCommandPrefix(context.squarePath, joinedName)} join --kick`,
98
+ ].join('\n'));
99
+ }
100
+ const afterSquare = await openSquare(context.squarePath, { clock: nowMs });
101
+ const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
102
+ await closeOpenSquare(afterSquare);
89
103
  recordLocalJoin(joinedName, context.squarePath);
90
104
  await sweepPendingNotifications(context.squarePath);
91
- const activities = renderPublicTail(after.acts, intent.lastN, nowMs(), joinedName);
92
- const contextText = preamble.join('\n').trim();
105
+ const activities = after.recentActivities.map((event) => renderAmbientEvent(event, joinedName, {
106
+ now: nowMs(),
107
+ preview: intent.lastN === null ? undefined : 200,
108
+ actNumber: event.kind === 'say' ? after.sayNumbers[event.index] : undefined,
109
+ })).filter(Boolean).join('\n\n');
110
+ const contextText = after.joinContext;
93
111
  const fallback = hasAutomaticDeliveryIdentity()
94
112
  ? []
95
113
  : ['', `» ${participantCommandPrefix(context.squarePath, joinedName)} catch --idle 30m`, ' no session delivery detected — keep this catch open for new activity'];
96
- const scene = after.warmup.join('\n').trim();
114
+ const scene = after.scene;
115
+ const entryLine = !isRejoin
116
+ ? '● You stepped into the square'
117
+ : reconnect && !intent.kick
118
+ ? '● you are already in the square'
119
+ : `✓ you banished the original ${participantIdentity(joinedName)} — the name is yours`;
97
120
  const output = [
98
- `● You stepped into the square`,
99
- ...(isRejoin || scene === '' ? [] : ['', scene]),
121
+ entryLine,
122
+ ...(reconnect || scene === '' ? [] : ['', scene]),
100
123
  ...(isRejoin || contextText === '' ? [] : ['', 'context', contextText]),
101
- ...(activities === '' ? [] : ['', 'recent activity', activities]),
124
+ ...(isRejoin || activities === '' ? [] : ['', 'recent activity', activities]),
102
125
  ...fallback,
103
126
  ].join('\n');
104
- return withPathOutput(context.squarePath, output, { participantCount: inSquareCount(after) });
127
+ return withPathOutput(context.squarePath, output, { participantCount: after.participantCount });
105
128
  }
106
- catch (error) {
107
- if (!(error instanceof SquareError) || error.code !== 'conflict')
108
- throw error;
109
- const doc = loadSquare(context.squarePath);
110
- const joinedName = resolveRosterName(doc, intent.name);
111
- if (joinedName === undefined || !isCurrentlyJoined(doc.acts, joinedName))
112
- throw error;
113
- const reconnect = localParticipantOwner(context.squarePath, joinedName) !== undefined;
114
- if (!intent.kick && !reconnect) {
115
- fail([
116
- `✕ ${joinedName} shoos you out of the square`,
117
- ` · a same-named participant stands here — the name is taken`,
118
- ` · --kick banishes her and the name becomes yours`,
119
- `» ${participantCommandPrefix(context.squarePath, joinedName)} join --kick`,
120
- ].join('\n'));
121
- }
122
- recordLocalJoin(joinedName, context.squarePath);
123
- await sweepPendingNotifications(context.squarePath);
124
- const fallback = hasAutomaticDeliveryIdentity()
125
- ? ''
126
- : `\n» ${participantCommandPrefix(context.squarePath, joinedName)} catch --idle 30m\n no session delivery detected — keep this catch open for new activity`;
127
- const line = reconnect && !intent.kick
128
- ? `● you are already in the square`
129
- : `✓ you banished the original ${joinedName} — the name is yours`;
130
- const scene = doc.warmup.join('\n').trim();
131
- const showScene = !(reconnect && !intent.kick) && scene !== '';
132
- return withPathOutput(context.squarePath, `${line}${showScene ? `\n\n${scene}` : ''}${fallback}`, { participantCount: inSquareCount(doc) });
129
+ finally {
130
+ await square.close();
133
131
  }
134
132
  },
135
133
  present: (result) => process.stdout.write(result),
@@ -195,10 +193,15 @@ export const doneCommand = {
195
193
  parse: parseDone,
196
194
  async execute(intent, context) {
197
195
  const body = resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim();
198
- const committed = await execute(context.squarePath, { type: 'done', name: intent.name, body, now: nowMs() });
199
- const name = committed.acts[0].actor;
196
+ const square = await Square.at({ path: context.squarePath, clock: nowMs, notifier: wakeNotifierForSquare(context.squarePath) });
197
+ const participant = await square.join(intent.name);
198
+ const result = await participant.done(body);
199
+ await square.close();
200
+ const name = result.activity.actor;
200
201
  recordLocalDone(name, context.squarePath);
201
- return withPathOutput(context.squarePath, `× ${name} steps out of the square — done · just now`, { participantCount: inSquareCount(loadSquare(context.squarePath)) });
202
+ const presentation = await openSquare(context.squarePath, { clock: nowMs });
203
+ const participantCount = (await entryPresentation(presentation, name).finally(() => closeOpenSquare(presentation))).participantCount;
204
+ return withPathOutput(context.squarePath, `○ ${participantIdentity(name)} steps out of the square — done · just now`, { participantCount });
202
205
  },
203
206
  present: (result) => process.stdout.write(result),
204
207
  };
@@ -210,9 +213,19 @@ function parseHold(argv, context) {
210
213
  export const holdCommand = {
211
214
  parse: parseHold,
212
215
  async execute(intent, context) {
213
- const committed = await execute(context.squarePath, { type: 'hold', actor: intent.name, body: resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim(), now: nowMs() });
214
- const doc = loadSquare(context.squarePath);
215
- return withPathOutput(context.squarePath, renderEventCli(committed.acts[0]), { participantCount: inSquareCount(doc), held: true });
216
+ const square = await Square.at({ path: context.squarePath, clock: nowMs, notifier: wakeNotifierForSquare(context.squarePath) });
217
+ try {
218
+ const participant = await square.join(intent.name);
219
+ const result = await participant.hold(resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim());
220
+ await square.close();
221
+ const presentationSquare = await openSquare(context.squarePath, { clock: nowMs });
222
+ const presentation = await eventPresentation(presentationSquare, result.activity.id);
223
+ await closeOpenSquare(presentationSquare);
224
+ return withPathOutput(context.squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount, held: true });
225
+ }
226
+ finally {
227
+ await square.close();
228
+ }
216
229
  },
217
230
  present: (result) => process.stdout.write(result),
218
231
  };
@@ -223,25 +236,19 @@ export const resumeCommand = {
223
236
  return { name: requireParticipant(context.name) };
224
237
  },
225
238
  async execute(intent, context) {
226
- const committed = await execute(context.squarePath, { type: 'resume', actor: intent.name, now: nowMs() });
227
- const doc = loadSquare(context.squarePath);
228
- return withPathOutput(context.squarePath, renderEventCli(committed.acts[0]), { participantCount: inSquareCount(doc) });
229
- },
230
- present: (result) => process.stdout.write(result),
231
- };
232
- export const compactCommand = {
233
- parse(argv, context) {
234
- let keep = 50;
235
- for (let index = 0; index < argv.length; index++) {
236
- if (argv[index] !== '--keep')
237
- usage(context.command);
238
- keep = parsePositiveInteger(requireValue(argv, index, argv[index]), argv[index]);
239
- index += 1;
239
+ const square = await Square.at({ path: context.squarePath, clock: nowMs, notifier: wakeNotifierForSquare(context.squarePath) });
240
+ try {
241
+ const participant = await square.join(intent.name);
242
+ const result = await participant.resume();
243
+ await square.close();
244
+ const presentationSquare = await openSquare(context.squarePath, { clock: nowMs });
245
+ const presentation = await eventPresentation(presentationSquare, result.activity.id);
246
+ await closeOpenSquare(presentationSquare);
247
+ return withPathOutput(context.squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount });
248
+ }
249
+ finally {
250
+ await square.close();
240
251
  }
241
- return { keep };
242
- },
243
- async execute(intent, context) {
244
- await cmdCompact(context.squarePath, intent);
245
252
  },
246
- present: () => { },
253
+ present: (result) => process.stdout.write(result),
247
254
  };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { InboxMembership } from './model.js';
2
+ export interface CodexHookInput {
3
+ session_id?: unknown;
4
+ hook_event_name?: unknown;
5
+ cwd?: unknown;
6
+ source?: unknown;
7
+ }
8
+ export declare function codexHookResponse(input: CodexHookInput, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<object | undefined>;
9
+ export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
10
+ export declare function runCodexHookAsync(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
@@ -1,13 +1,14 @@
1
1
  import { presentPendingAtBoundary } from './boundary-presentation.js';
2
2
  import { sessionInbox } from './inbox.js';
3
- export function codexHookResponse(input, lookup = sessionInbox, env = process.env) {
3
+ import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
4
+ export async function codexHookResponse(input, lookup = sessionInbox, env = process.env) {
4
5
  if (typeof input.session_id !== 'string' || input.session_id === '')
5
6
  return undefined;
6
7
  if (input.hook_event_name !== 'PostToolUse')
7
8
  return undefined;
8
9
  return presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }), lookup, env);
9
10
  }
10
- export function runCodexHook(inputText, env = process.env) {
11
+ export async function runCodexHook(inputText, env = process.env) {
11
12
  let input;
12
13
  try {
13
14
  input = JSON.parse(inputText);
@@ -17,6 +18,37 @@ export function runCodexHook(inputText, env = process.env) {
17
18
  }
18
19
  if (input === null || typeof input !== 'object')
19
20
  return '';
20
- const response = codexHookResponse(input, sessionInbox, env);
21
+ const response = await codexHookResponse(input, sessionInbox, env);
21
22
  return response === undefined ? '' : `${JSON.stringify(response)}\n`;
22
23
  }
24
+ export async function runCodexHookAsync(inputText, env = process.env) {
25
+ let input;
26
+ try {
27
+ input = JSON.parse(inputText);
28
+ }
29
+ catch {
30
+ return '';
31
+ }
32
+ if (input === null || typeof input !== 'object')
33
+ return '';
34
+ const value = input;
35
+ if (typeof value.session_id !== 'string' || typeof value.cwd !== 'string')
36
+ return runCodexHook(inputText, env);
37
+ if (value.hook_event_name === 'SessionStart' || value.hook_event_name === 'SessionResume') {
38
+ try {
39
+ const context = await automaticSessionStart('codex', value.session_id, value.cwd, env);
40
+ return context === undefined ? '' : `${JSON.stringify({ hookSpecificOutput: { hookEventName: value.hook_event_name, additionalContext: context } })}\n`;
41
+ }
42
+ catch {
43
+ return '';
44
+ }
45
+ }
46
+ if (value.hook_event_name === 'SessionEnd') {
47
+ try {
48
+ await automaticSessionEnd('codex', value.session_id, value.cwd, env);
49
+ }
50
+ catch { /* end remains bounded */ }
51
+ return '';
52
+ }
53
+ return runCodexHook(inputText, env);
54
+ }
@@ -0,0 +1,92 @@
1
+ import { type ActivitiesOptions, type Act, type StoredAct, type SquareState, type Reach, type HardCap } from './model.js';
2
+ import { peerPublicActs, peerRoomChanges } from './activity-feed.js';
3
+ export interface UnreadActivitySummary {
4
+ name: string;
5
+ count: number;
6
+ latestActivityAgeMs: number;
7
+ previews: UnreadActivityPreview[];
8
+ }
9
+ export interface UnreadActivityPreview {
10
+ number: number;
11
+ act: Extract<StoredAct, {
12
+ kind: 'say';
13
+ }>;
14
+ }
15
+ export declare function resolveKnownName(squareState: SquareState, name: string): string;
16
+ export interface DecideJoinResult {
17
+ joinedName: string;
18
+ addParticipant: boolean;
19
+ joinAct?: Extract<Act, {
20
+ kind: 'join';
21
+ }>;
22
+ }
23
+ export declare function decideJoin(squareState: SquareState, name: string, now: number): DecideJoinResult;
24
+ export type ActDecision = {
25
+ type: 'sent';
26
+ act: Act;
27
+ confirmation: string;
28
+ ownActCount: number;
29
+ pendingPublic: ReturnType<typeof peerPublicActs>;
30
+ pendingRoomChanges: ReturnType<typeof peerRoomChanges>;
31
+ } | {
32
+ type: 'blocked';
33
+ activitySummaries: UnreadActivitySummary[];
34
+ unreadRoomChanges: ReturnType<typeof peerRoomChanges>;
35
+ } | {
36
+ type: 'capped';
37
+ count: number;
38
+ hardCap: number;
39
+ } | {
40
+ type: 'throttled';
41
+ delayMs: number;
42
+ } | {
43
+ type: 'held';
44
+ reason: string | undefined;
45
+ } | {
46
+ type: 'bell_quota';
47
+ nextAt: number;
48
+ };
49
+ export declare function decideAct(squareState: SquareState, input: {
50
+ name: string;
51
+ body: string;
52
+ force: boolean;
53
+ now: number;
54
+ reach?: Reach;
55
+ reply?: number;
56
+ }): ActDecision;
57
+ export declare function coreDone(squareState: SquareState, name: string, body: string, now: number): Extract<Act, {
58
+ kind: 'done';
59
+ }>;
60
+ export declare function coreHold(squareState: SquareState, actor: string, body: string, now: number): Extract<Act, {
61
+ kind: 'hold';
62
+ }>;
63
+ export declare function coreResume(squareState: SquareState, actor: string, now: number): Extract<Act, {
64
+ kind: 'resume';
65
+ }>;
66
+ export interface ParticipantStatus {
67
+ name: string;
68
+ state: 'active' | 'done' | 'not joined';
69
+ activityCount: number;
70
+ lastActiveAt: number | undefined;
71
+ presence: CorePresenceState;
72
+ presenceAt: number | undefined;
73
+ unreadActivityCount: number;
74
+ pendingMentionCount: number;
75
+ }
76
+ export type CorePresenceState = 'never-joined' | 'active' | 'watching' | 'done';
77
+ export interface StatusResult {
78
+ hardCap: HardCap;
79
+ throttlePerMinute: number | undefined;
80
+ activeCount: number;
81
+ doneCount: number;
82
+ holdActive: boolean;
83
+ holdReason: string | undefined;
84
+ holdActor: string | undefined;
85
+ holdAt: number | undefined;
86
+ participants: ParticipantStatus[];
87
+ latestAct: StoredAct | undefined;
88
+ now: number;
89
+ }
90
+ export declare function coreStatus(squareState: SquareState, now: number): StatusResult;
91
+ export declare function coreParticipants(squareState: SquareState, now: number): ParticipantStatus[];
92
+ export declare function coreActivities(squareState: SquareState, opts: ActivitiesOptions): StoredAct[];
package/dist/decisions.js CHANGED
@@ -1,28 +1,30 @@
1
1
  import { SquareError, sameName, validateName, } from './model.js';
2
+ import { participantIdentity } from './participant-identity.js';
2
3
  import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, getReadState, publicActs, readCursor, resolveRosterName, rosterNames, matchesMentionTarget, THROTTLE_WINDOW_MS, } from './runtime.js';
3
4
  import { actDelta, peerPublicActs, peerRoomChanges } from './activity-feed.js';
4
5
  import { extractMentions, formatActivityId, validate } from './square-core.js';
5
6
  import { deriveDeliveryModel } from './delivery.js';
6
7
  import { compileSearchPattern } from './search.js';
7
- export function resolveKnownName(doc, name) {
8
+ export function resolveKnownName(squareState, name) {
8
9
  validateName(name);
9
- const known = resolveRosterName(doc, name);
10
+ const known = resolveRosterName(squareState, name);
10
11
  if (known === undefined) {
11
- const roster = rosterNames(doc);
12
- throw new SquareError('invalid_args', `Unknown participant "${name}". Expected one of: ${roster.join(', ')}.`);
12
+ const roster = rosterNames(squareState);
13
+ throw new SquareError('invalid_args', `Unknown participant "${participantIdentity(name)}". Expected one of: ${roster.map(participantIdentity).join(', ')}.`);
13
14
  }
14
15
  return known;
15
16
  }
16
17
  function participantState(state, name) {
17
18
  return state.participants.find((participant) => sameName(participant.name, name));
18
19
  }
19
- export function decideJoin(doc, name, now) {
20
- const knownName = resolveRosterName(doc, name);
20
+ export function decideJoin(squareState, name, now) {
21
+ validateName(name);
22
+ const knownName = resolveRosterName(squareState, name);
21
23
  const joinedName = knownName ?? name;
22
- const state = foldedState(doc);
24
+ const state = foldedState(squareState);
23
25
  const result = validate(state, { kind: 'join', actor: joinedName, at: now });
24
26
  if (!result.ok && result.reason === 'already_joined') {
25
- throw new SquareError('conflict', `A participant named "${joinedName}" is already in this square.`);
27
+ return { joinedName, addParticipant: false };
26
28
  }
27
29
  return {
28
30
  joinedName,
@@ -30,22 +32,39 @@ export function decideJoin(doc, name, now) {
30
32
  joinAct: { kind: 'join', actor: joinedName, at: now },
31
33
  };
32
34
  }
35
+ function resolveStandingName(squareState, requestedName) {
36
+ validateName(requestedName);
37
+ const name = resolveRosterName(squareState, requestedName);
38
+ if (name === undefined)
39
+ throw new SquareError('not_joined', `${participantIdentity(requestedName)} has not joined this square`);
40
+ return name;
41
+ }
42
+ function requireStanding(squareState, act) {
43
+ const result = validate(foldedState(squareState), act);
44
+ if (result.ok)
45
+ return;
46
+ if (result.reason === 'done')
47
+ throw new SquareError('already_done', `${act.actor === undefined ? 'participant' : participantIdentity(act.actor)} is already done`);
48
+ if (result.reason === 'not_joined')
49
+ throw new SquareError('not_joined', `${act.actor === undefined ? 'participant' : participantIdentity(act.actor)} has not joined this square`);
50
+ throw new Error(`Unexpected standing validation result: ${result.reason}`);
51
+ }
33
52
  const UNREAD_PREVIEW_LIMIT = 3;
34
- export function decideAct(doc, input) {
53
+ export function decideAct(squareState, input) {
35
54
  const { now, force } = input;
36
- const name = resolveKnownName(doc, input.name);
55
+ const name = resolveStandingName(squareState, input.name);
37
56
  const body = input.body;
38
57
  if (body.trim() === '')
39
58
  throw new SquareError('invalid_args', 'express body cannot be empty');
40
59
  const reach = input.reach;
41
60
  const reply = input.reply;
42
61
  if (reply !== undefined) {
43
- if (!Number.isSafeInteger(reply) || reply < 0 || reply >= doc.runtime.nextActIndex) {
62
+ if (!Number.isSafeInteger(reply) || reply < 0 || reply >= squareState.runtime.nextActIndex) {
44
63
  const label = Number.isSafeInteger(reply) && reply >= 0 ? formatActivityId(reply) : String(reply);
45
64
  throw new SquareError('invalid_args', `Unknown reply activity: ${label}`);
46
65
  }
47
66
  }
48
- const state = foldedState(doc);
67
+ const state = foldedState(squareState);
49
68
  if (reach !== 'bell'
50
69
  && extractMentions(body).length === 0) {
51
70
  throw new SquareError('invalid_args', 'express requires an @mention unless using --bell');
@@ -55,10 +74,10 @@ export function decideAct(doc, input) {
55
74
  kind: 'say', actor: name, at: now, body,
56
75
  ...(reach !== undefined ? { reach } : {}),
57
76
  ...(reply !== undefined ? { reply } : {}),
58
- }, { hardCap: doc.hardCap, throttlePerMinute: doc.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
77
+ }, { hardCap: squareState.hardCap, throttlePerMinute: squareState.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
59
78
  if (!result.ok) {
60
79
  if (result.reason === 'done')
61
- throw new SquareError('conflict', `${name} is done; rejoin to express again`);
80
+ throw new SquareError('already_done', `${name} is already done`);
62
81
  if (result.reason === 'held')
63
82
  return { type: 'held', reason: result.hold.reason };
64
83
  if (result.reason === 'hard_cap')
@@ -68,9 +87,9 @@ export function decideAct(doc, input) {
68
87
  if (result.reason === 'bell_quota')
69
88
  return { type: 'bell_quota', nextAt: result.nextAt };
70
89
  if (result.reason === 'not_joined')
71
- throw new SquareError('conflict', `${name} has not joined this square`);
90
+ throw new SquareError('not_joined', `${name} has not joined this square`);
72
91
  }
73
- const delta = actDelta(doc.acts, readCursor(doc, name));
92
+ const delta = actDelta(squareState.acts, readCursor(squareState, name));
74
93
  const unreadPublic = peerPublicActs(delta, name);
75
94
  const unreadRoomChanges = peerRoomChanges(delta, name);
76
95
  const sayCountByActor = new Map();
@@ -122,21 +141,29 @@ export function decideAct(doc, input) {
122
141
  pendingRoomChanges: unreadRoomChanges,
123
142
  };
124
143
  }
125
- export function coreDone(doc, name, body, now) {
126
- const resolvedName = resolveKnownName(doc, name);
127
- return { kind: 'done', actor: resolvedName, at: now, body: body.replace(/\r\n/g, '\n').trim() };
144
+ export function coreDone(squareState, name, body, now) {
145
+ const resolvedName = resolveStandingName(squareState, name);
146
+ const act = { kind: 'done', actor: resolvedName, at: now, body: body.replace(/\r\n/g, '\n').trim() };
147
+ requireStanding(squareState, act);
148
+ return act;
128
149
  }
129
- export function coreHold(_doc, actor, body, now) {
130
- return { kind: 'hold', actor, at: now, body: body.replace(/\r\n/g, '\n').trim() };
150
+ export function coreHold(squareState, actor, body, now) {
151
+ const resolvedName = resolveStandingName(squareState, actor);
152
+ const act = { kind: 'hold', actor: resolvedName, at: now, body: body.replace(/\r\n/g, '\n').trim() };
153
+ requireStanding(squareState, act);
154
+ return act;
131
155
  }
132
- export function coreResume(_doc, actor, now) {
133
- return { kind: 'resume', actor, at: now };
156
+ export function coreResume(squareState, actor, now) {
157
+ const resolvedName = resolveStandingName(squareState, actor);
158
+ const act = { kind: 'resume', actor: resolvedName, at: now };
159
+ requireStanding(squareState, act);
160
+ return act;
134
161
  }
135
- function presenceFor(doc, snapshot, name, now) {
162
+ function presenceFor(squareState, snapshot, name, now) {
136
163
  if (snapshot?.done)
137
164
  return { state: 'done', lastAt: snapshot.lastActiveAt };
138
- const cursor = getReadState(doc, name);
139
- const lease = freshWatchLease(doc, name, now);
165
+ const cursor = getReadState(squareState, name);
166
+ const lease = freshWatchLease(squareState, name, now);
140
167
  if (lease !== undefined)
141
168
  return { state: 'watching', lastAt: cursor?.updatedAt ?? lease.heartbeatAt };
142
169
  const lastAt = cursor?.updatedAt ?? (snapshot?.joined ? snapshot.lastActiveAt : undefined);
@@ -144,16 +171,16 @@ function presenceFor(doc, snapshot, name, now) {
144
171
  ? { state: 'never-joined', lastAt: undefined }
145
172
  : { state: 'active', lastAt };
146
173
  }
147
- function buildParticipantStatuses(doc, now, state = foldedState(doc)) {
148
- const delivery = deriveDeliveryModel(doc);
174
+ function buildParticipantStatuses(squareState, now, state = foldedState(squareState)) {
175
+ const delivery = deriveDeliveryModel(squareState);
149
176
  return state.participants.map((snapshot) => {
150
177
  const participant = snapshot.name;
151
- const presence = presenceFor(doc, snapshot, participant, now);
178
+ const presence = presenceFor(squareState, snapshot, participant, now);
152
179
  const participantStatus = snapshot?.done ? 'done' : snapshot?.joined ? 'active' : 'not joined';
153
- const consumedThrough = readCursor(doc, participant);
180
+ const consumedThrough = readCursor(squareState, participant);
154
181
  let unreadActivityCount = 0;
155
182
  if (snapshot?.joined) {
156
- for (const act of doc.acts) {
183
+ for (const act of squareState.acts) {
157
184
  if (actStableIndex(act) <= consumedThrough || act.actor === undefined || sameName(act.actor, participant))
158
185
  continue;
159
186
  if (act.kind !== 'read')
@@ -172,43 +199,44 @@ function buildParticipantStatuses(doc, now, state = foldedState(doc)) {
172
199
  };
173
200
  });
174
201
  }
175
- export function coreStatus(doc, now) {
176
- const state = foldedState(doc);
177
- const latestAct = publicActs(doc.acts).at(-1);
202
+ export function coreStatus(squareState, now) {
203
+ const state = foldedState(squareState);
204
+ const latestAct = publicActs(squareState.acts).at(-1);
178
205
  return {
179
- hardCap: doc.hardCap,
180
- throttlePerMinute: doc.throttlePerMinute,
206
+ hardCap: squareState.hardCap,
207
+ throttlePerMinute: squareState.throttlePerMinute,
181
208
  activeCount: state.joined.length,
182
209
  doneCount: state.done.length,
183
210
  holdActive: state.hold.active,
184
211
  holdReason: state.hold.reason,
185
212
  holdActor: state.hold.actor,
186
213
  holdAt: state.hold.at,
187
- participants: buildParticipantStatuses(doc, now, state),
214
+ participants: buildParticipantStatuses(squareState, now, state),
188
215
  latestAct,
189
216
  now,
190
217
  };
191
218
  }
192
- export function coreParticipants(doc, now) {
193
- return buildParticipantStatuses(doc, now);
219
+ export function coreParticipants(squareState, now) {
220
+ return buildParticipantStatuses(squareState, now);
194
221
  }
195
- export function coreActivities(doc, opts) {
222
+ export function coreActivities(squareState, opts) {
196
223
  const participants = opts.participants ?? [];
197
- const canonicalParticipants = participants.map((participant) => resolveKnownName(doc, participant));
198
- const viewer = opts.viewer !== undefined ? resolveKnownName(doc, opts.viewer) : undefined;
199
- let acts = [...doc.acts];
200
- // --at establishes a context window first; other filters AND inside it.
201
- if (opts.atIndex != null) {
224
+ const canonicalParticipants = participants.map((participant) => resolveKnownName(squareState, participant));
225
+ const viewer = opts.viewer !== undefined ? resolveKnownName(squareState, opts.viewer) : undefined;
226
+ let acts = [...squareState.acts];
227
+ // --at establishes one or more context windows first; other filters AND inside their union.
228
+ if (opts.atIndexes !== undefined && opts.atIndexes.length > 0) {
202
229
  const before = opts.beforeContext ?? 0;
203
230
  const after = opts.afterContext ?? 0;
204
- const centerPos = acts.findIndex((act) => act.index === opts.atIndex);
205
- if (centerPos < 0)
206
- return [];
207
- acts = acts.slice(Math.max(0, centerPos - before), centerPos + after + 1);
208
- }
209
- if (opts.ids !== undefined && opts.ids.length > 0) {
210
- const wanted = new Set(opts.ids);
211
- acts = acts.filter((act) => wanted.has(act.index));
231
+ const selected = new Set();
232
+ for (const atIndex of opts.atIndexes) {
233
+ const centerPos = acts.findIndex((act) => act.index === atIndex);
234
+ if (centerPos < 0)
235
+ continue;
236
+ for (const act of acts.slice(Math.max(0, centerPos - before), centerPos + after + 1))
237
+ selected.add(act.index);
238
+ }
239
+ acts = acts.filter((act) => selected.has(act.index));
212
240
  }
213
241
  if (opts.afterIndex != null)
214
242
  acts = acts.filter((act) => act.index > opts.afterIndex);
@@ -220,13 +248,13 @@ export function coreActivities(doc, opts) {
220
248
  if (opts.after != null)
221
249
  acts = acts.filter((act) => act.at > opts.after);
222
250
  if (opts.mention != null) {
223
- const mention = resolveKnownName(doc, opts.mention);
251
+ const mention = resolveKnownName(squareState, opts.mention);
224
252
  acts = acts.filter((act) => act.kind === 'say' && (act.reach === 'bell' || matchesMentionTarget(act, mention)));
225
253
  }
226
254
  if (opts.pending) {
227
255
  if (viewer === undefined)
228
256
  return [];
229
- const pendingIndexes = new Set(deriveDeliveryModel(doc).pendingFor(viewer).map((notification) => notification.item.index));
257
+ const pendingIndexes = new Set(deriveDeliveryModel(squareState).pendingFor(viewer).map((notification) => notification.item.index));
230
258
  acts = acts.filter((act) => pendingIndexes.has(act.index));
231
259
  }
232
260
  const search = opts.grep !== undefined ? { pattern: opts.grep, fixed: false } : opts.fixed !== undefined ? { pattern: opts.fixed, fixed: true } : undefined;
@@ -249,25 +277,3 @@ export function coreActivities(doc, opts) {
249
277
  }
250
278
  return acts;
251
279
  }
252
- export function coreCompact(doc, keep) {
253
- if (doc.acts.length <= keep)
254
- return { archived: [], doc };
255
- const splitAt = doc.acts.length - keep;
256
- const archived = doc.acts.slice(0, splitAt);
257
- const retained = doc.acts.slice(splitAt);
258
- const cutoffIndex = actStableIndex(archived[archived.length - 1]);
259
- const unread = foldedState(doc).participants
260
- .filter((participant) => participant.joined && readCursor(doc, participant.name) < cutoffIndex)
261
- .map((participant) => participant.name);
262
- if (unread.length > 0) {
263
- throw new SquareError('conflict', `Refusing to compact: ${unread.join(', ')} ${unread.length === 1 ? 'has' : 'have'} not read through the activities being archived.`);
264
- }
265
- return {
266
- archived,
267
- doc: {
268
- ...doc,
269
- acts: retained,
270
- runtime: doc.runtime,
271
- },
272
- };
273
- }