@astrosheep/square 0.3.14 → 0.3.16

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 (62) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  2. package/dist/activity-feed.d.ts +2 -3
  3. package/dist/activity-feed.js +6 -15
  4. package/dist/activity.js +1 -1
  5. package/dist/artifact.js +17 -26
  6. package/dist/attention-presentation.d.ts +13 -0
  7. package/dist/attention-presentation.js +22 -0
  8. package/dist/automatic-session.js +6 -8
  9. package/dist/boundary-presentation.js +35 -14
  10. package/dist/claude-hook.js +5 -3
  11. package/dist/cli/context.d.ts +4 -4
  12. package/dist/cli/context.js +25 -4
  13. package/dist/cli/harness-command.js +1 -1
  14. package/dist/cli/maintenance-commands.js +5 -4
  15. package/dist/cli/observation-commands.js +24 -16
  16. package/dist/cli/program.js +1 -1
  17. package/dist/cli/registry.js +4 -1
  18. package/dist/cli/square-commands.d.ts +7 -0
  19. package/dist/cli/square-commands.js +123 -27
  20. package/dist/codex-hook.js +5 -3
  21. package/dist/decisions.d.ts +19 -0
  22. package/dist/decisions.js +45 -12
  23. package/dist/delivery.d.ts +14 -21
  24. package/dist/delivery.js +40 -74
  25. package/dist/help.js +8 -5
  26. package/dist/inbox.js +1 -0
  27. package/dist/index.d.ts +1 -10
  28. package/dist/index.js +0 -32
  29. package/dist/landing.d.ts +12 -0
  30. package/dist/landing.js +35 -4
  31. package/dist/model.d.ts +8 -11
  32. package/dist/model.js +2 -2
  33. package/dist/notifications.js +35 -11
  34. package/dist/paseo-delivery.js +7 -4
  35. package/dist/paseo.d.ts +6 -0
  36. package/dist/paseo.js +5 -0
  37. package/dist/presence.d.ts +3 -0
  38. package/dist/presence.js +40 -10
  39. package/dist/presentation.d.ts +6 -2
  40. package/dist/presentation.js +22 -9
  41. package/dist/presented.d.ts +2 -0
  42. package/dist/presented.js +20 -0
  43. package/dist/registry.js +4 -1
  44. package/dist/routes.d.ts +5 -0
  45. package/dist/routes.js +17 -0
  46. package/dist/runtime.d.ts +4 -5
  47. package/dist/runtime.js +35 -18
  48. package/dist/square-core.d.ts +17 -0
  49. package/dist/square-core.js +51 -1
  50. package/dist/square-facade.d.ts +9 -1
  51. package/dist/square-wiring.d.ts +8 -0
  52. package/dist/square-wiring.js +32 -3
  53. package/dist/views.d.ts +9 -3
  54. package/dist/views.js +16 -14
  55. package/dist/wake-port.d.ts +4 -1
  56. package/dist/wake-port.js +5 -0
  57. package/dist/wakes.js +3 -5
  58. package/dist/watch.js +1 -0
  59. package/package.json +19 -3
  60. package/skills/brainstorm/SKILL.md +1 -1
  61. package/skills/square/.claude-plugin/plugin.json +1 -1
  62. package/skills/square/SKILL.md +1 -1
@@ -6,9 +6,9 @@ import { sweepPendingNotifications, wakeNotifierForSquare } from '../notificatio
6
6
  import { nowMs } from '../runtime.js';
7
7
  import { createSquare, openSquare } from '../square-file-adapter.js';
8
8
  import { closeOpenSquare } from '../open-square.js';
9
- import { Square } from '../square-wiring.js';
10
- import { entryPresentation, eventPresentation } from '../views.js';
11
- import { fail, parseHardCap, parsePositiveInteger, readPipedBodyFallback, readStdinSync, requireParticipant, requireValue, resolveBody, usage, } from './context.js';
9
+ import { openParticipant, Square } from '../square-wiring.js';
10
+ import { admitsBareExpress, entryPresentation, eventPresentation } from '../views.js';
11
+ import { fail, parseHardCap, parsePositiveInteger, readPipedBodyFallback, readStdinSync, requireParticipant, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
12
12
  function parseBuild(argv) {
13
13
  const options = { force: false, hardCap: null };
14
14
  for (let index = 0; index < argv.length; index++) {
@@ -49,10 +49,11 @@ function parseBuild(argv) {
49
49
  export const buildCommand = {
50
50
  parse: (argv) => parseBuild(argv),
51
51
  async execute(intent, context) {
52
- await createSquare(context.squarePath, intent.options, intent.snippet);
52
+ const squarePath = requireSquarePath(context);
53
+ await createSquare(squarePath, intent.options, intent.snippet);
53
54
  const cap = intent.options.hardCap === null ? 'unlimited' : formatHardCap(intent.options.hardCap);
54
55
  const throttle = intent.options.throttlePerMinute === undefined ? [] : [` · throttle ${intent.options.throttlePerMinute}/min`];
55
- return withPathOutput(context.squarePath, ['✓ built', ` · cap ${cap}`, ...throttle, ' · participants (none seeded — first join adds names)'].join('\n'), { participantCount: 0 });
56
+ return withPathOutput(squarePath, ['✓ built', ` · cap ${cap}`, ...throttle, ' · participants (none seeded — first join adds names)'].join('\n'), { participantCount: 0 });
56
57
  },
57
58
  present: (result) => process.stdout.write(result),
58
59
  };
@@ -79,38 +80,40 @@ function parseJoin(argv, context) {
79
80
  export const joinCommand = {
80
81
  parse: parseJoin,
81
82
  async execute(intent, context) {
82
- const beforeSquare = await openSquare(context.squarePath, { clock: nowMs });
83
+ const squarePath = requireSquarePath(context);
84
+ const beforeSquare = await openSquare(squarePath, { clock: nowMs });
83
85
  const before = await entryPresentation(beforeSquare, intent.name, intent.lastN);
84
86
  await closeOpenSquare(beforeSquare);
85
- const square = await Square.at({ path: context.squarePath, clock: nowMs, notifier: wakeNotifierForSquare(context.squarePath) });
87
+ const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
86
88
  try {
87
89
  const participant = await square.join(intent.name);
88
90
  const joinedName = participant.name;
89
91
  const isRejoin = before.joined;
90
92
  const reconnect = isRejoin
91
- && localParticipantOwner(context.squarePath, joinedName) !== undefined;
93
+ && localParticipantOwner(squarePath, joinedName) !== undefined;
92
94
  if (isRejoin && !intent.kick && !reconnect) {
93
95
  fail([
94
96
  `✕ ${participantIdentity(joinedName)} shoos you out of the square`,
95
97
  ` · a same-named participant stands here — the name is taken`,
96
98
  ` · --kick banishes her and the name becomes yours`,
97
- `» ${participantCommandPrefix(context.squarePath, joinedName)} join --kick`,
99
+ `» ${participantCommandPrefix(squarePath, joinedName)} join --kick`,
98
100
  ].join('\n'));
99
101
  }
100
- const afterSquare = await openSquare(context.squarePath, { clock: nowMs });
102
+ const afterSquare = await openSquare(squarePath, { clock: nowMs });
101
103
  const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
102
104
  await closeOpenSquare(afterSquare);
103
- recordLocalJoin(joinedName, context.squarePath);
104
- await sweepPendingNotifications(context.squarePath);
105
+ recordLocalJoin(joinedName, squarePath);
106
+ await sweepPendingNotifications(squarePath);
105
107
  const activities = after.recentActivities.map((event) => renderAmbientEvent(event, joinedName, {
106
108
  now: nowMs(),
107
109
  preview: intent.lastN === null ? undefined : 200,
108
110
  actNumber: event.kind === 'say' ? after.sayNumbers[event.index] : undefined,
111
+ squareState: after.state,
109
112
  })).filter(Boolean).join('\n\n');
110
113
  const contextText = after.joinContext;
111
114
  const fallback = hasAutomaticDeliveryIdentity()
112
115
  ? []
113
- : ['', `» ${participantCommandPrefix(context.squarePath, joinedName)} catch --idle 30m`, ' no session delivery detected — keep this catch open for new activity'];
116
+ : ['', `» ${participantCommandPrefix(squarePath, joinedName)} catch --idle 30m`, ' no session delivery detected — keep this catch open for new activity'];
114
117
  const scene = after.scene;
115
118
  const entryLine = !isRejoin
116
119
  ? '● You stepped into the square'
@@ -124,7 +127,7 @@ export const joinCommand = {
124
127
  ...(isRejoin || activities === '' ? [] : ['', 'recent activity', activities]),
125
128
  ...fallback,
126
129
  ].join('\n');
127
- return withPathOutput(context.squarePath, output, { participantCount: after.participantCount });
130
+ return withPathOutput(squarePath, output, { participantCount: after.participantCount });
128
131
  }
129
132
  finally {
130
133
  await square.close();
@@ -172,18 +175,108 @@ function parseActivity(argv, context) {
172
175
  export const expressCommand = {
173
176
  parse: parseActivity,
174
177
  async execute(intent, context) {
175
- await sweepPendingNotifications(context.squarePath);
178
+ const squarePath = requireSquarePath(context);
179
+ await sweepPendingNotifications(squarePath);
180
+ const body = resolveBody(intent.activity);
181
+ if (!intent.force && intent.reach !== 'bell') {
182
+ const reader = await openSquare(squarePath, { clock: nowMs });
183
+ try {
184
+ if (!await admitsBareExpress(reader, intent.name, body)) {
185
+ fail(`✕ no one is turned toward you\n · address someone with @name, ring the bell, or use --force\n» ${participantCommandPrefix(squarePath, intent.name)} express --force -`);
186
+ }
187
+ }
188
+ finally {
189
+ await closeOpenSquare(reader);
190
+ }
191
+ }
176
192
  const reachArg = intent.reach === 'bell' ? ' --bell' : '';
177
- await cmdActivity(context.squarePath, intent.name, intent.activity, resolveBody, {
193
+ await cmdActivity(squarePath, intent.name, body, (value) => value, {
178
194
  force: intent.force,
179
195
  noWait: intent.noWait,
180
196
  reach: intent.reach,
181
197
  reply: intent.reply,
182
- forceCommand: `${participantCommandPrefix(context.squarePath, intent.name)} express --force${reachArg}${intent.reply === undefined ? '' : ` --reply ${formatActivityId(intent.reply)}`} -`,
198
+ forceCommand: `${participantCommandPrefix(squarePath, intent.name)} express --force${reachArg}${intent.reply === undefined ? '' : ` --reply ${formatActivityId(intent.reply)}`} -`,
183
199
  });
184
200
  },
185
201
  present: () => { },
186
202
  };
203
+ function parseListener(argv, context, targetRequired) {
204
+ if (targetRequired ? argv.length !== 1 : argv.length !== 0)
205
+ usage(context.command);
206
+ return { name: requireParticipant(context.name), ...(targetRequired ? { target: argv[0] } : {}) };
207
+ }
208
+ function listenerPresentation(squarePath, actor, target, verb, activity, participantCount) {
209
+ if (activity !== null) {
210
+ const action = verb === 'listen'
211
+ ? `${participantIdentity(actor)} turns an ear toward ${participantIdentity(target)}`
212
+ : `${participantIdentity(actor)} turns away from ${participantIdentity(target)}`;
213
+ return withPathOutput(squarePath, `· ${action}`, { participantCount });
214
+ }
215
+ const action = verb === 'listen'
216
+ ? `${participantIdentity(actor)} already turns an ear toward ${participantIdentity(target)}`
217
+ : `${participantIdentity(actor)} is not turned toward ${participantIdentity(target)}`;
218
+ return withPathOutput(squarePath, `○ ${action}`, { participantCount });
219
+ }
220
+ export const listenCommand = {
221
+ parse(argv, context) { return parseListener(argv, context, true); },
222
+ async execute(intent, context) {
223
+ const squarePath = requireSquarePath(context);
224
+ const square = await Square.at({ path: squarePath, clock: nowMs });
225
+ const facade = await openParticipant({ path: squarePath, clock: nowMs }, intent.name);
226
+ try {
227
+ const participant = facade.participant;
228
+ const result = await participant.listen(intent.target);
229
+ const participantCount = (await square.snapshot()).participants.filter((item) => item.state === 'joined').length;
230
+ return listenerPresentation(squarePath, participant.name, intent.target, 'listen', result.activity, participantCount);
231
+ }
232
+ finally {
233
+ await facade.close();
234
+ await square.close();
235
+ }
236
+ },
237
+ present: (result) => process.stdout.write(result),
238
+ };
239
+ export const ignoreCommand = {
240
+ parse(argv, context) { return parseListener(argv, context, true); },
241
+ async execute(intent, context) {
242
+ const squarePath = requireSquarePath(context);
243
+ const square = await Square.at({ path: squarePath, clock: nowMs });
244
+ const facade = await openParticipant({ path: squarePath, clock: nowMs }, intent.name);
245
+ try {
246
+ const participant = facade.participant;
247
+ const result = await participant.ignore(intent.target);
248
+ const participantCount = (await square.snapshot()).participants.filter((item) => item.state === 'joined').length;
249
+ return listenerPresentation(squarePath, participant.name, intent.target, 'ignore', result.activity, participantCount);
250
+ }
251
+ finally {
252
+ await facade.close();
253
+ await square.close();
254
+ }
255
+ },
256
+ present: (result) => process.stdout.write(result),
257
+ };
258
+ export const listeningCommand = {
259
+ parse(argv, context) { return parseListener(argv, context, false); },
260
+ async execute(intent, context) {
261
+ const squarePath = requireSquarePath(context);
262
+ const square = await Square.at({ path: squarePath, clock: nowMs });
263
+ const facade = await openParticipant({ path: squarePath, clock: nowMs }, intent.name);
264
+ try {
265
+ const participant = facade.participant;
266
+ const targets = await participant.listening();
267
+ const participantCount = (await square.snapshot()).participants.filter((item) => item.state === 'joined').length;
268
+ const body = targets.length === 0
269
+ ? `○ ${participantIdentity(participant.name)} is not turned toward anyone`
270
+ : ['listening', ...targets.map((target) => ` · ${participantIdentity(target)}`)].join('\n');
271
+ return withPathOutput(squarePath, body, { participantCount });
272
+ }
273
+ finally {
274
+ await facade.close();
275
+ await square.close();
276
+ }
277
+ },
278
+ present: (result) => process.stdout.write(result),
279
+ };
187
280
  function parseDone(argv, context) {
188
281
  if (argv.length > 1)
189
282
  usage(context.command);
@@ -192,16 +285,17 @@ function parseDone(argv, context) {
192
285
  export const doneCommand = {
193
286
  parse: parseDone,
194
287
  async execute(intent, context) {
288
+ const squarePath = requireSquarePath(context);
195
289
  const body = resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim();
196
- const square = await Square.at({ path: context.squarePath, clock: nowMs, notifier: wakeNotifierForSquare(context.squarePath) });
290
+ const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
197
291
  const participant = await square.join(intent.name);
198
292
  const result = await participant.done(body);
199
293
  await square.close();
200
294
  const name = result.activity.actor;
201
- recordLocalDone(name, context.squarePath);
202
- const presentation = await openSquare(context.squarePath, { clock: nowMs });
295
+ recordLocalDone(name, squarePath);
296
+ const presentation = await openSquare(squarePath, { clock: nowMs });
203
297
  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 });
298
+ return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · just now`, { participantCount });
205
299
  },
206
300
  present: (result) => process.stdout.write(result),
207
301
  };
@@ -213,15 +307,16 @@ function parseHold(argv, context) {
213
307
  export const holdCommand = {
214
308
  parse: parseHold,
215
309
  async execute(intent, context) {
216
- const square = await Square.at({ path: context.squarePath, clock: nowMs, notifier: wakeNotifierForSquare(context.squarePath) });
310
+ const squarePath = requireSquarePath(context);
311
+ const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
217
312
  try {
218
313
  const participant = await square.join(intent.name);
219
314
  const result = await participant.hold(resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim());
220
315
  await square.close();
221
- const presentationSquare = await openSquare(context.squarePath, { clock: nowMs });
316
+ const presentationSquare = await openSquare(squarePath, { clock: nowMs });
222
317
  const presentation = await eventPresentation(presentationSquare, result.activity.id);
223
318
  await closeOpenSquare(presentationSquare);
224
- return withPathOutput(context.squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount, held: true });
319
+ return withPathOutput(squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount, held: true });
225
320
  }
226
321
  finally {
227
322
  await square.close();
@@ -236,15 +331,16 @@ export const resumeCommand = {
236
331
  return { name: requireParticipant(context.name) };
237
332
  },
238
333
  async execute(intent, context) {
239
- const square = await Square.at({ path: context.squarePath, clock: nowMs, notifier: wakeNotifierForSquare(context.squarePath) });
334
+ const squarePath = requireSquarePath(context);
335
+ const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
240
336
  try {
241
337
  const participant = await square.join(intent.name);
242
338
  const result = await participant.resume();
243
339
  await square.close();
244
- const presentationSquare = await openSquare(context.squarePath, { clock: nowMs });
340
+ const presentationSquare = await openSquare(squarePath, { clock: nowMs });
245
341
  const presentation = await eventPresentation(presentationSquare, result.activity.id);
246
342
  await closeOpenSquare(presentationSquare);
247
- return withPathOutput(context.squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount });
343
+ return withPathOutput(squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount });
248
344
  }
249
345
  finally {
250
346
  await square.close();
@@ -32,11 +32,12 @@ export async function runCodexHookAsync(inputText, env = process.env) {
32
32
  if (input === null || typeof input !== 'object')
33
33
  return '';
34
34
  const value = input;
35
- if (typeof value.session_id !== 'string' || typeof value.cwd !== 'string')
35
+ if (typeof value.session_id !== 'string')
36
36
  return runCodexHook(inputText, env);
37
37
  if (value.hook_event_name === 'SessionStart' || value.hook_event_name === 'SessionResume') {
38
+ const cwd = typeof value.cwd === 'string' ? value.cwd : process.cwd();
38
39
  try {
39
- const context = await automaticSessionStart('codex', value.session_id, value.cwd, env);
40
+ const context = await automaticSessionStart('codex', value.session_id, cwd, env);
40
41
  return context === undefined ? '' : `${JSON.stringify({ hookSpecificOutput: { hookEventName: value.hook_event_name, additionalContext: context } })}\n`;
41
42
  }
42
43
  catch {
@@ -44,8 +45,9 @@ export async function runCodexHookAsync(inputText, env = process.env) {
44
45
  }
45
46
  }
46
47
  if (value.hook_event_name === 'SessionEnd') {
48
+ const cwd = typeof value.cwd === 'string' ? value.cwd : process.cwd();
47
49
  try {
48
- await automaticSessionEnd('codex', value.session_id, value.cwd, env);
50
+ await automaticSessionEnd('codex', value.session_id, cwd, env);
49
51
  }
50
52
  catch { /* end remains bounded */ }
51
53
  return '';
@@ -1,5 +1,6 @@
1
1
  import { type ActivitiesOptions, type Act, type StoredAct, type SquareState, type Reach, type HardCap } from './model.js';
2
2
  import { peerPublicActs, peerRoomChanges } from './activity-feed.js';
3
+ import { type Perception } from './square-core.js';
3
4
  export interface UnreadActivitySummary {
4
5
  name: string;
5
6
  count: number;
@@ -11,6 +12,7 @@ export interface UnreadActivityPreview {
11
12
  act: Extract<StoredAct, {
12
13
  kind: 'say';
13
14
  }>;
15
+ perception: Perception;
14
16
  }
15
17
  export declare function resolveKnownName(squareState: SquareState, name: string): string;
16
18
  export interface DecideJoinResult {
@@ -20,7 +22,16 @@ export interface DecideJoinResult {
20
22
  kind: 'join';
21
23
  }>;
22
24
  }
25
+ export interface DecideImplicitJoinResult {
26
+ joinedName: string;
27
+ state: 'joined' | 'active' | 'done';
28
+ joinAct?: Extract<Act, {
29
+ kind: 'join';
30
+ }>;
31
+ }
23
32
  export declare function decideJoin(squareState: SquareState, name: string, now: number): DecideJoinResult;
33
+ /** Automatic presence may enter only once; an explicit join remains the re-entry path. */
34
+ export declare function decideImplicitJoin(squareState: SquareState, name: string, now: number): DecideImplicitJoinResult;
24
35
  export type ActDecision = {
25
36
  type: 'sent';
26
37
  act: Act;
@@ -57,6 +68,13 @@ export declare function decideAct(squareState: SquareState, input: {
57
68
  export declare function coreDone(squareState: SquareState, name: string, body: string, now: number): Extract<Act, {
58
69
  kind: 'done';
59
70
  }>;
71
+ export declare function coreListen(squareState: SquareState, actor: string, target: string, now: number): Extract<Act, {
72
+ kind: 'listen';
73
+ }> | undefined;
74
+ export declare function coreIgnore(squareState: SquareState, actor: string, target: string, now: number): Extract<Act, {
75
+ kind: 'ignore';
76
+ }> | undefined;
77
+ export declare function coreListening(squareState: SquareState, actor: string): readonly string[];
60
78
  export declare function coreHold(squareState: SquareState, actor: string, body: string, now: number): Extract<Act, {
61
79
  kind: 'hold';
62
80
  }>;
@@ -66,6 +84,7 @@ export declare function coreResume(squareState: SquareState, actor: string, now:
66
84
  export interface ParticipantStatus {
67
85
  name: string;
68
86
  state: 'active' | 'done' | 'not joined';
87
+ listening: readonly string[];
69
88
  activityCount: number;
70
89
  lastActiveAt: number | undefined;
71
90
  presence: CorePresenceState;
package/dist/decisions.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { SquareError, sameName, validateName, } from './model.js';
2
2
  import { participantIdentity } from './participant-identity.js';
3
- import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, getReadState, publicActs, readCursor, resolveRosterName, rosterNames, matchesMentionTarget, THROTTLE_WINDOW_MS, } from './runtime.js';
3
+ import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs, readCursor, resolveRosterName, rosterNames, THROTTLE_WINDOW_MS, } from './runtime.js';
4
4
  import { actDelta, peerPublicActs, peerRoomChanges } from './activity-feed.js';
5
- import { extractMentions, formatActivityId, validate } from './square-core.js';
6
- import { deriveDeliveryModel } from './delivery.js';
5
+ import { formatActivityId, isListening, listeningTo, validate } from './square-core.js';
6
+ import { deriveDeliveryModel, perceiveActivity } from './delivery.js';
7
7
  import { compileSearchPattern } from './search.js';
8
8
  export function resolveKnownName(squareState, name) {
9
9
  validateName(name);
@@ -32,6 +32,21 @@ export function decideJoin(squareState, name, now) {
32
32
  joinAct: { kind: 'join', actor: joinedName, at: now },
33
33
  };
34
34
  }
35
+ /** Automatic presence may enter only once; an explicit join remains the re-entry path. */
36
+ export function decideImplicitJoin(squareState, name, now) {
37
+ validateName(name);
38
+ const knownName = resolveRosterName(squareState, name);
39
+ const joinedName = knownName ?? name;
40
+ if (knownName !== undefined) {
41
+ const current = participantState(foldedState(squareState), knownName);
42
+ return { joinedName, state: current?.joined === true ? 'active' : 'done' };
43
+ }
44
+ return {
45
+ joinedName,
46
+ state: 'joined',
47
+ joinAct: { kind: 'join', actor: joinedName, at: now },
48
+ };
49
+ }
35
50
  function resolveStandingName(squareState, requestedName) {
36
51
  validateName(requestedName);
37
52
  const name = resolveRosterName(squareState, requestedName);
@@ -65,10 +80,6 @@ export function decideAct(squareState, input) {
65
80
  }
66
81
  }
67
82
  const state = foldedState(squareState);
68
- if (reach !== 'bell'
69
- && extractMentions(body).length === 0) {
70
- throw new SquareError('invalid_args', 'express requires an @mention unless using --bell');
71
- }
72
83
  const current = participantState(state, name);
73
84
  const result = validate(state, {
74
85
  kind: 'say', actor: name, at: now, body,
@@ -108,7 +119,7 @@ export function decideAct(squareState, input) {
108
119
  latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
109
120
  previews: [
110
121
  ...(currentSummary?.previews ?? []),
111
- { number: sayCountByActor.get(actorKey) ?? 1, act: item },
122
+ { number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: perceiveActivity(squareState, item, name) },
112
123
  ].slice(-UNREAD_PREVIEW_LIMIT),
113
124
  });
114
125
  }
@@ -147,6 +158,26 @@ export function coreDone(squareState, name, body, now) {
147
158
  requireStanding(squareState, act);
148
159
  return act;
149
160
  }
161
+ export function coreListen(squareState, actor, target, now) {
162
+ const resolvedActor = resolveStandingName(squareState, actor);
163
+ validateName(target);
164
+ const state = foldedState(squareState);
165
+ const act = { kind: 'listen', actor: resolvedActor, target, at: now };
166
+ requireStanding(squareState, act);
167
+ return isListening(state, resolvedActor, target) ? undefined : act;
168
+ }
169
+ export function coreIgnore(squareState, actor, target, now) {
170
+ const resolvedActor = resolveStandingName(squareState, actor);
171
+ validateName(target);
172
+ const state = foldedState(squareState);
173
+ const act = { kind: 'ignore', actor: resolvedActor, target, at: now };
174
+ requireStanding(squareState, act);
175
+ return isListening(state, resolvedActor, target) ? act : undefined;
176
+ }
177
+ export function coreListening(squareState, actor) {
178
+ const resolvedActor = resolveStandingName(squareState, actor);
179
+ return listeningTo(foldedState(squareState), resolvedActor);
180
+ }
150
181
  export function coreHold(squareState, actor, body, now) {
151
182
  const resolvedName = resolveStandingName(squareState, actor);
152
183
  const act = { kind: 'hold', actor: resolvedName, at: now, body: body.replace(/\r\n/g, '\n').trim() };
@@ -162,11 +193,11 @@ export function coreResume(squareState, actor, now) {
162
193
  function presenceFor(squareState, snapshot, name, now) {
163
194
  if (snapshot?.done)
164
195
  return { state: 'done', lastAt: snapshot.lastActiveAt };
165
- const cursor = getReadState(squareState, name);
196
+ const cursorAt = squareState.acts.findLast((act) => act.index <= readCursor(squareState, name))?.at;
166
197
  const lease = freshWatchLease(squareState, name, now);
167
198
  if (lease !== undefined)
168
- return { state: 'watching', lastAt: cursor?.updatedAt ?? lease.heartbeatAt };
169
- const lastAt = cursor?.updatedAt ?? (snapshot?.joined ? snapshot.lastActiveAt : undefined);
199
+ return { state: 'watching', lastAt: cursorAt ?? lease.heartbeatAt };
200
+ const lastAt = cursorAt ?? (snapshot?.joined ? snapshot.lastActiveAt : undefined);
170
201
  return lastAt === undefined
171
202
  ? { state: 'never-joined', lastAt: undefined }
172
203
  : { state: 'active', lastAt };
@@ -190,6 +221,7 @@ function buildParticipantStatuses(squareState, now, state = foldedState(squareSt
190
221
  return {
191
222
  name: participant,
192
223
  state: participantStatus,
224
+ listening: listeningTo(state, participant),
193
225
  activityCount: snapshot?.activityCount ?? 0,
194
226
  lastActiveAt: snapshot?.lastActiveAt,
195
227
  presence: presence.state,
@@ -249,7 +281,8 @@ export function coreActivities(squareState, opts) {
249
281
  acts = acts.filter((act) => act.at > opts.after);
250
282
  if (opts.mention != null) {
251
283
  const mention = resolveKnownName(squareState, opts.mention);
252
- acts = acts.filter((act) => act.kind === 'say' && (act.reach === 'bell' || matchesMentionTarget(act, mention)));
284
+ const delivery = deriveDeliveryModel(squareState);
285
+ acts = acts.filter((act) => act.kind === 'say' && delivery.plan(act).some((item) => sameName(item.recipient, mention)));
253
286
  }
254
287
  if (opts.pending) {
255
288
  if (viewer === undefined)
@@ -1,4 +1,5 @@
1
- import { type DeliveryReceipt, type DirectedNotificationRoute, type SquareState, type SquareRuntimeState, type StoredAct, type WatchLease, type WatchLeaseFilter, type WakeRouteKind, type Reach } from './model.js';
1
+ import { type DirectedNotificationRoute, type SquareState, type StoredAct, type WatchLease, type WatchLeaseFilter, type WakeRouteKind, type Reach } from './model.js';
2
+ import { type Perception } from './square-core.js';
2
3
  export type { DirectedNotificationRoute } from './model.js';
3
4
  export type SayItem = StoredAct & {
4
5
  kind: 'say';
@@ -28,6 +29,12 @@ export type WakeDispatchResult = {
28
29
  signature: string;
29
30
  message: string;
30
31
  diagnostic?: unknown;
32
+ } | {
33
+ outcome: 'unavailable';
34
+ signature: string;
35
+ message: string;
36
+ diagnostic?: unknown;
37
+ retainRoute?: boolean;
31
38
  } | {
32
39
  outcome: 'cancelled';
33
40
  };
@@ -39,43 +46,29 @@ export interface RoutedNotification {
39
46
  actor: string;
40
47
  body: string;
41
48
  route: DirectedNotificationRoute;
49
+ recipient?: string;
42
50
  }
43
51
  export interface CatchFilterShape {
44
52
  actor: string;
45
53
  body: string;
46
54
  reach?: Reach;
55
+ recipients?: readonly string[];
47
56
  }
48
57
  export interface DeliveryModel {
49
58
  plan(item: StoredAct): PlannedNotification[];
50
59
  pendingFor(recipient: string): PlannedNotification[];
51
60
  }
52
- /** Delivery receipts are the only durable acknowledgement of directed attention. */
53
- export declare function deliveryReceipt(squareState: SquareState, name: string, actOrIndex: StoredAct | number): DeliveryReceipt | undefined;
54
- export declare function deliveryReceiptFromRuntime(runtime: SquareRuntimeState, recipient: string, actOrIndex: StoredAct | number): DeliveryReceipt | undefined;
55
- export declare function isDeliveryDelivered(squareState: SquareState, name: string, actOrIndex: StoredAct | number): boolean;
56
- export declare function recordDeliveredDelivery(squareState: SquareState, name: string, actOrIndex: StoredAct | number, receipt: Omit<DeliveryReceipt, 'status'>): boolean;
57
- export declare function recordDeliveredRuntime(runtime: SquareRuntimeState, recipient: string, actOrIndex: StoredAct | number, receipt: Omit<DeliveryReceipt, 'status'>): boolean;
58
- export declare function markDeliveredDelivery(squareState: SquareState, name: string, actOrIndex: StoredAct | number, at?: number): boolean;
61
+ export declare function isActivitySeen(squareState: SquareState, name: string, actOrIndex: StoredAct | number): boolean;
59
62
  /**
60
63
  * Derive delivery behavior once from the decoded Square state.
61
64
  * All consumers share these targets instead of reinterpreting artifact text or cursor state.
62
65
  */
63
66
  export declare function deriveDeliveryModel(squareState: SquareState): DeliveryModel;
64
67
  export declare function planActNotifications(squareState: SquareState, item: StoredAct): PlannedNotification[];
65
- /** Mark only the directed notifications selected by the canonical pending projection. */
66
- export declare function markDeliveredNotifications(squareState: SquareState, recipient: string, delivered: StoredAct[], at?: number): boolean;
68
+ export declare function perceiveActivity(squareState: SquareState, item: StoredAct, viewer: string): Perception;
69
+ /** Mark only the notifications selected by the canonical catch projection as fully seen. */
70
+ export declare function markSeenNotifications(squareState: SquareState, recipient: string, delivered: StoredAct[], at?: number): boolean;
67
71
  /** Canonical say-activity filter shared by catch selection and hook ownership. */
68
72
  export declare function matchesCatchFilter(activity: CatchFilterShape, filter: WatchLeaseFilter): boolean;
69
73
  /** True only when the live catch's own filters would deliver this notification. */
70
74
  export declare function leaseOwnsNotification(lease: WatchLease, notification: RoutedNotification): boolean;
71
- export type SquareRoute = Readonly<unknown>;
72
- interface ParsedSquareRoute {
73
- readonly v: 1;
74
- readonly squarePath: string;
75
- readonly name: string;
76
- }
77
- export declare function parseSquareRoute(value: unknown): ParsedSquareRoute;
78
- export declare function captureRoute(input: {
79
- cwd: string;
80
- env?: NodeJS.ProcessEnv;
81
- }): SquareRoute | null;