@astrosheep/square 0.3.70 → 0.3.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/activity-feed.d.ts +4 -5
  2. package/dist/activity-feed.js +14 -10
  3. package/dist/artifact.d.ts +26 -33
  4. package/dist/artifact.js +346 -317
  5. package/dist/attention-presentation.js +2 -9
  6. package/dist/automatic-session.js +6 -6
  7. package/dist/boundary-presentation.js +3 -3
  8. package/dist/catch-decisions.d.ts +19 -12
  9. package/dist/catch-decisions.js +32 -68
  10. package/dist/claude-delivery.js +7 -7
  11. package/dist/claude-mod.js +18 -7
  12. package/dist/cli/catch-presentation.d.ts +7 -0
  13. package/dist/cli/catch-presentation.js +55 -0
  14. package/dist/cli/context.d.ts +1 -1
  15. package/dist/cli/context.js +5 -23
  16. package/dist/cli/express-presentation.d.ts +10 -0
  17. package/dist/cli/express-presentation.js +196 -0
  18. package/dist/cli/maintenance-commands.d.ts +4 -0
  19. package/dist/cli/maintenance-commands.js +29 -6
  20. package/dist/cli/observation-commands.d.ts +7 -5
  21. package/dist/cli/observation-commands.js +275 -262
  22. package/dist/cli/program.d.ts +1 -1
  23. package/dist/cli/program.js +43 -56
  24. package/dist/cli/registry.js +2 -1
  25. package/dist/cli/square-commands.d.ts +3 -9
  26. package/dist/cli/square-commands.js +134 -255
  27. package/dist/codex-queue.js +4 -1
  28. package/dist/decisions.d.ts +26 -74
  29. package/dist/decisions.js +190 -335
  30. package/dist/delivery-health.js +13 -22
  31. package/dist/delivery-operations.d.ts +0 -11
  32. package/dist/delivery-operations.js +44 -33
  33. package/dist/delivery.d.ts +33 -32
  34. package/dist/delivery.js +70 -81
  35. package/dist/help.js +6 -1
  36. package/dist/inbox.d.ts +27 -0
  37. package/dist/inbox.js +55 -18
  38. package/dist/index.d.ts +2 -4
  39. package/dist/index.js +1 -1
  40. package/dist/list.d.ts +7 -1
  41. package/dist/list.js +49 -122
  42. package/dist/mcp.d.ts +1 -1
  43. package/dist/mcp.js +7 -37
  44. package/dist/model.d.ts +4 -44
  45. package/dist/model.js +1 -3
  46. package/dist/notifications.d.ts +1 -1
  47. package/dist/notifications.js +13 -7
  48. package/dist/open-square.d.ts +16 -2
  49. package/dist/open-square.js +40 -1
  50. package/dist/packages/agent-delivery/src/claude.d.ts +2 -11
  51. package/dist/packages/agent-delivery/src/index.d.ts +2 -42
  52. package/dist/packages/agent-delivery/src/opencode.d.ts +2 -11
  53. package/dist/packages/agent-delivery/src/paseo.d.ts +2 -13
  54. package/dist/packages/agent-delivery/src/pi-client.d.ts +2 -9
  55. package/dist/packages/agent-delivery/src/types.d.ts +42 -0
  56. package/dist/participant-host.d.ts +6 -3
  57. package/dist/participant-host.js +44 -38
  58. package/dist/perception-projection.d.ts +6 -15
  59. package/dist/perception-projection.js +6 -26
  60. package/dist/plugins/opencode/delivery.js +9 -9
  61. package/dist/plugins/opencode/index.js +3 -3
  62. package/dist/ports.d.ts +156 -11
  63. package/dist/presentation-operations.d.ts +1 -1
  64. package/dist/presentation-operations.js +35 -44
  65. package/dist/presentation.d.ts +12 -19
  66. package/dist/presentation.js +33 -54
  67. package/dist/registry.d.ts +2 -0
  68. package/dist/registry.js +1 -1
  69. package/dist/routes.d.ts +7 -14
  70. package/dist/routes.js +34 -72
  71. package/dist/runtime.d.ts +12 -39
  72. package/dist/runtime.js +33 -164
  73. package/dist/square-actions.d.ts +11 -21
  74. package/dist/square-actions.js +171 -275
  75. package/dist/square-application.d.ts +31 -55
  76. package/dist/square-application.js +625 -141
  77. package/dist/square-core.d.ts +3 -94
  78. package/dist/square-core.js +8 -353
  79. package/dist/square-domain.d.ts +24 -0
  80. package/dist/square-domain.js +77 -0
  81. package/dist/square-facade.d.ts +360 -9
  82. package/dist/square-migration.d.ts +24 -0
  83. package/dist/square-migration.js +211 -0
  84. package/dist/square-projections.d.ts +2 -42
  85. package/dist/square-projections.js +1 -37
  86. package/dist/square-sqlite.d.ts +59 -0
  87. package/dist/square-sqlite.js +702 -0
  88. package/dist/stream.d.ts +3 -10
  89. package/dist/stream.js +81 -52
  90. package/dist/views.d.ts +27 -95
  91. package/dist/views.js +217 -87
  92. package/dist/wake-eligibility.d.ts +15 -5
  93. package/dist/wake-eligibility.js +32 -16
  94. package/dist/wakes.d.ts +3 -3
  95. package/dist/wakes.js +31 -27
  96. package/guides/application-facade-delivery-evidence.md +200 -0
  97. package/guides/application-facade-design.md +95 -0
  98. package/guides/change-waits.md +4 -4
  99. package/guides/migration.md +77 -0
  100. package/guides/postcommit-simplification-performance.md +36 -0
  101. package/guides/primitive-catch-consolidation.md +85 -0
  102. package/guides/primitive-history-consolidation.md +76 -0
  103. package/guides/primitive-status-consolidation.md +87 -0
  104. package/guides/primitive-storage-consolidation.md +128 -0
  105. package/guides/row-runtime-delivery-evidence.md +160 -0
  106. package/guides/row-storage-design.md +567 -0
  107. package/package.json +1 -1
  108. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  109. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  110. package/plugins/opencode/delivery.ts +11 -11
  111. package/plugins/opencode/index.ts +3 -3
  112. package/plugins/paseo/client/square-message.tsx +77 -64
  113. package/plugins/paseo/shared/message.ts +16 -0
  114. package/plugins/pi/square-pi.js +5 -1
  115. package/dist/activity.d.ts +0 -14
  116. package/dist/activity.js +0 -248
  117. package/dist/square-file-adapter.d.ts +0 -26
  118. package/dist/square-file-adapter.js +0 -85
  119. package/dist/square-state.d.ts +0 -6
  120. package/dist/square-state.js +0 -138
  121. package/dist/square-storage.d.ts +0 -14
  122. package/dist/square-storage.js +0 -188
  123. package/dist/square-wiring.d.ts +0 -33
  124. package/dist/square-wiring.js +0 -109
  125. package/dist/watch.d.ts +0 -8
  126. package/dist/watch.js +0 -252
@@ -1,65 +1,56 @@
1
1
  import { formatActivityId, parseActivityId } from './square-core.js';
2
- import { deriveDeliveryModel } from './delivery.js';
3
- import { recordObservation } from './runtime.js';
4
- /** Present one pending activity; artifact seen remains the authoritative receipt. */
2
+ import { pendingHead, sameJoin } from './delivery.js';
3
+ /** Presentation evidence is not a receipt; only the fenced receipt transaction marks seen. */
5
4
  export async function presentPending(input) {
6
5
  const index = typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
7
6
  if (index === undefined)
8
7
  return { presented: false };
9
- let before;
10
- try {
11
- before = await input.artifact.read(input.signal);
12
- }
13
- catch {
14
- return { presented: false };
15
- }
16
- const delivery = deriveDeliveryModel(before.state);
17
- const item = before.state.acts.find((activity) => activity.index === index);
18
- if (input.current?.(before.state) === false || item === undefined || delivery.isSeen(input.participant, index) || !delivery.pendingFor(input.participant).some((notification) => notification.item.index === index))
8
+ const before = await input.artifact.read((facts) => {
9
+ if (input.current?.(facts) === false || !pendingHead(facts, input.participant, index))
10
+ return undefined;
11
+ return { item: facts.activity(index), participant: facts.participant(input.participant) };
12
+ }, input.signal).catch(() => undefined);
13
+ if (before?.value === undefined)
19
14
  return { presented: false };
15
+ const { item, participant } = before.value;
16
+ const current = (facts) => sameJoin(facts, participant) && input.current?.(facts) !== false
17
+ && pendingHead(facts, participant.name, index) !== undefined;
18
+ const coordinate = { location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation' };
20
19
  let claimToken;
20
+ const release = async () => {
21
+ if (claimToken !== undefined)
22
+ await input.hostLedger.releaseEvidence({ ...coordinate, claimToken });
23
+ };
21
24
  if (input.hostLedger !== undefined && input.session !== undefined) {
22
- const claim = await input.hostLedger.claimEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', leaseMs: input.timeoutMs ?? 5000 });
25
+ const claim = await input.hostLedger.claimEvidence({ ...coordinate, leaseMs: input.timeoutMs ?? 5000 });
23
26
  if (claim.status !== 'acquired')
24
27
  return { presented: false };
25
28
  claimToken = claim.claimToken;
26
- let current;
27
- try {
28
- current = await input.artifact.read(input.signal);
29
- }
30
- catch {
31
- await input.hostLedger.releaseEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', claimToken }).catch(() => undefined);
32
- return { presented: false };
33
- }
34
- if (input.current?.(current.state) === false || deriveDeliveryModel(current.state).isSeen(input.participant, index)) {
35
- await input.hostLedger.releaseEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', claimToken }).catch(() => undefined);
29
+ const fresh = await input.artifact.read(current, input.signal).catch(() => undefined);
30
+ if (!fresh?.value) {
31
+ await release().catch(() => undefined);
36
32
  return { presented: false };
37
33
  }
38
- try {
39
- await input.sink.present(item);
40
- }
41
- catch (error) {
42
- await input.hostLedger.appendEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', outcome: 'failed', message: error instanceof Error ? error.message : String(error), claimToken });
43
- throw error;
44
- }
45
34
  }
46
- else
35
+ try {
47
36
  await input.sink.present(item);
48
- const committed = await input.artifact.transact((state) => {
49
- const current = deriveDeliveryModel(state);
50
- if (input.current?.(state) === false || current.isSeen(input.participant, index) || !current.pendingFor(input.participant).some((notification) => notification.item.index === index))
51
- return { result: false };
52
- if (input.markSeen === false)
53
- return { result: true };
54
- const changed = recordObservation(state, input.participant, index, 'seen', input.now ?? Date.now());
55
- return changed ? { state, result: true } : { result: false };
37
+ }
38
+ catch (error) {
39
+ if (claimToken !== undefined)
40
+ await input.hostLedger.appendEvidence({ ...coordinate, outcome: 'failed', message: error instanceof Error ? error.message : String(error), claimToken });
41
+ throw error;
42
+ }
43
+ const { value: committed } = await input.artifact.transact((facts) => {
44
+ if (!current(facts))
45
+ return false;
46
+ return input.markSeen === false || facts.markSeen(participant.name, [index], input.now ?? Date.now()) > 0;
56
47
  }, input.signal);
57
48
  if (!committed) {
58
- if (input.hostLedger !== undefined && input.session !== undefined && claimToken !== undefined)
59
- await input.hostLedger.releaseEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', claimToken });
49
+ await release();
60
50
  return { presented: false };
61
51
  }
62
- if (input.hostLedger !== undefined && input.session !== undefined && claimToken !== undefined)
63
- await input.hostLedger.appendEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', outcome: input.markSeen === false ? 'clipped' : 'presented', ...(input.markSeen === false ? { message: 'presentation clipped' } : {}), claimToken });
52
+ if (claimToken !== undefined)
53
+ await input.hostLedger.appendEvidence({ ...coordinate, outcome: input.markSeen === false ? 'clipped' : 'presented',
54
+ ...(input.markSeen === false ? { message: 'presentation clipped' } : {}), claimToken });
64
55
  return { presented: true, activity: item };
65
56
  }
@@ -1,7 +1,7 @@
1
- import { type StoredAct, type SquareState, type PublicAct, type RoomChangeAct } from './model.js';
1
+ import { type StoredAct, type RoomChangeAct } from './model.js';
2
2
  export { participantIdentity } from './participant-identity.js';
3
3
  import { type Perception } from './square-core.js';
4
- import type { UnreadActivitySummary, ParticipantStatus } from './decisions.js';
4
+ import type { Activity, UnreadActivitySummary, ParticipantSummary } from './square-facade.js';
5
5
  export type WatchStatus = 'stale' | 'empty-now' | 'quorum' | 'capped';
6
6
  interface HeaderOptions {
7
7
  participantCount?: number;
@@ -23,7 +23,7 @@ interface ActivityLimitOptions extends ParticipantOutputOptions {
23
23
  interface ActivityBlockedOptions extends ParticipantOutputOptions {
24
24
  retryCommand: string;
25
25
  forceCommand: string;
26
- activitySummaries: UnreadActivitySummary[];
26
+ activitySummaries: readonly UnreadActivitySummary[];
27
27
  draftPath?: string;
28
28
  }
29
29
  interface ExpressWaitingOptions {
@@ -44,7 +44,7 @@ interface WatchStatusOptions extends ParticipantOutputOptions {
44
44
  status: WatchStatus;
45
45
  idleMs?: number;
46
46
  presence?: {
47
- participants: ParticipantStatus[];
47
+ participants: readonly ParticipantSummary[];
48
48
  now: number;
49
49
  };
50
50
  showCatchHint?: boolean;
@@ -58,6 +58,7 @@ export declare function formatRefusal(squarePath: string, bodyLines: string[], o
58
58
  participantCount?: number;
59
59
  held?: boolean;
60
60
  }): string;
61
+ export declare function refusalText(error: unknown): string;
61
62
  export declare function quoteShell(value: string): string;
62
63
  export declare function commandPrefix(squarePath: string): string;
63
64
  export declare function participantCommandPrefix(squarePath: string, name: string): string;
@@ -66,44 +67,42 @@ export declare function joinRecoveryCommand(squarePath: string, name: string): s
66
67
  /** Taking a name is an explicit recovery choice, never an automatic retry. */
67
68
  export declare function takeoverRecoveryLines(squarePath: string, name: string): string[];
68
69
  export declare function participantsRecoveryCommand(squarePath: string): string;
69
- export declare function renderPresenceLines(participants: ParticipantStatus[], now: number, limit?: number): string[];
70
+ export declare function renderPresenceLines(recent: readonly ParticipantSummary[], now: number, limit?: number): string[];
70
71
  export declare function expressHintLine(ownActivityCount: number): string | undefined;
71
72
  export declare function truncateChars(body: string, maxChars: number): {
72
73
  text: string;
73
74
  remaining: number;
74
75
  };
75
76
  export declare function truncateExternalDiagnostic(diagnostic: string): string;
76
- export declare function renderRoomChangeText(event: RoomChangeAct): string;
77
+ export declare function renderRoomChangeText(event: RoomChangeAct | Activity): string;
77
78
  interface RenderedEvent {
78
79
  text: string;
79
80
  /** The rendered body was cut short by `preview`. */
80
81
  bodyClipped: boolean;
81
82
  }
82
- export declare function renderEventCli(event: StoredAct, opts?: {
83
+ export declare function renderEventCli(event: StoredAct | Activity, opts?: {
83
84
  now?: number;
84
85
  preview?: number;
85
86
  actNumber?: number;
86
87
  mention?: string;
87
88
  }): string;
88
- export declare function renderAmbientEventFacts(event: StoredAct, viewer: string, opts?: {
89
+ export declare function renderAmbientEventFacts(event: StoredAct | Activity, viewer: string, opts?: {
89
90
  now?: number;
90
91
  preview?: number;
91
92
  presencePreview?: number;
92
93
  actNumber?: number;
93
94
  mention?: string;
94
- squareState?: SquareState;
95
95
  perception?: Perception;
96
96
  }): RenderedEvent;
97
- export declare function renderAmbientEvent(event: StoredAct, viewer: string, opts?: {
97
+ export declare function renderAmbientEvent(event: StoredAct | Activity, viewer: string, opts?: {
98
98
  now?: number;
99
99
  preview?: number;
100
100
  presencePreview?: number;
101
101
  actNumber?: number;
102
102
  mention?: string;
103
- squareState?: SquareState;
104
103
  perception?: Perception;
105
104
  }): string;
106
- export declare function renderPendingFeed(history: StoredAct[], publicItems: PublicAct[], viewer: string | undefined, squareState: SquareState): string;
105
+ export declare function renderPendingFeed(publicItems: readonly StoredAct[], sayNumbers: Readonly<Record<number, number>>, perceptions: Readonly<Record<number, Perception>>, viewer: string): string;
107
106
  export declare function renderActivityBlocked(opts: ActivityBlockedOptions): string;
108
107
  /** An indeterminate or already-committed send only points to inspection, never resend. */
109
108
  export declare function renderActivityUncertain(opts: ParticipantOutputOptions & {
@@ -114,7 +113,7 @@ export declare function renderActivityUncertain(opts: ParticipantOutputOptions &
114
113
  export declare function renderExpressWaiting(opts: ExpressWaitingOptions): string;
115
114
  export declare function renderExpressNoWait(opts: ExpressNoWaitOptions): string;
116
115
  export declare function renderPresenceAnchor(names: readonly string[]): string;
117
- export declare function renderGrepActivitiesView(visible: StoredAct[], totalMatches: number, noTruncate: boolean | undefined, squarePath: string, pattern: string, fixed?: boolean, perception?: (act: StoredAct) => Perception): string;
116
+ export declare function renderGrepActivitiesView(visible: readonly StoredAct[], totalMatches: number, noTruncate: boolean | undefined, squarePath: string, pattern: string, fixed?: boolean, perception?: (act: StoredAct) => Perception): string;
118
117
  export declare function renderActivityLimit(opts: ActivityLimitOptions): string;
119
118
  export declare function renderWatchAlreadyActive(opts: ParticipantOutputOptions): string;
120
119
  export declare function renderWatchForceTakeover(_opts: ParticipantOutputOptions): string;
@@ -123,9 +122,3 @@ export declare function renderWatchReplaced(_opts: ParticipantOutputOptions): st
123
122
  export declare function renderWatchStatus(opts: WatchStatusOptions): string;
124
123
  export declare function renderDoctorClean(): string;
125
124
  export declare function renderDoctorUnfixable(reason: string): string;
126
- export declare function renderWatchOutput(history: StoredAct[], publicItems: PublicAct[], opts: {
127
- mention?: string;
128
- viewer: string;
129
- squareState?: SquareState;
130
- perceptions?: ReadonlyMap<number, Perception>;
131
- }): string;
@@ -1,13 +1,11 @@
1
- import { sameName } from './model.js';
1
+ import { sameName, isSquareError } from './model.js';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { participantIdentity } from './participant-identity.js';
5
5
  export { participantIdentity } from './participant-identity.js';
6
- import { audienceIncludes, audienceOf, MAX_IDENTITY_SET_SIZE } from './square-core.js';
7
- import { perceiveActivity } from './delivery.js';
8
- import { actId, sayNumberFor, HELD_WAIT_BUDGET_MS } from './runtime.js';
6
+ import { formatActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
7
+ import { HELD_WAIT_BUDGET_MS } from './runtime.js';
9
8
  import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
10
- import { compareParticipantActivity } from './decisions.js';
11
9
  import { grepSnippet } from './search.js';
12
10
  import { style } from './tty-style.js';
13
11
  export function displayPath(squarePath, cwd = process.cwd()) {
@@ -39,6 +37,11 @@ export function formatRefusal(squarePath, bodyLines, opts = {}) {
39
37
  return body === '' ? '' : `${body}\n`;
40
38
  return withPathOutput(squarePath, body, { ...opts, plain: true });
41
39
  }
40
+ export function refusalText(error) {
41
+ const text = error instanceof Error ? error.message : String(error);
42
+ const marked = isSquareError(error) && ['never_joined', 'not_standing', 'unmatched_mention'].includes(error.facts?.reason ?? '');
43
+ return marked ? text.split('\n').map((line) => `✕ ${line}`).join('\n') : text;
44
+ }
42
45
  export function quoteShell(value) {
43
46
  return `'${value.replace(/'/g, `'\\''`)}'`;
44
47
  }
@@ -69,11 +72,7 @@ function formatAge(ms) {
69
72
  return `${Math.max(0, ms)}ms`;
70
73
  return `${Math.floor(ms / 1000)}s`;
71
74
  }
72
- const PRESENCE_WINDOW_MS = 8 * 60 * 60 * 1000;
73
- export function renderPresenceLines(participants, now, limit = 5) {
74
- const recent = participants
75
- .filter((p) => p.state === 'done' || p.presence === 'watching' || (p.lastActiveAt !== undefined && now - p.lastActiveAt <= PRESENCE_WINDOW_MS))
76
- .sort(compareParticipantActivity);
75
+ export function renderPresenceLines(recent, now, limit = 5) {
77
76
  const shown = recent.slice(0, limit);
78
77
  if (shown.length === 0)
79
78
  return [style('dim', ' ○ nobody nearby')];
@@ -137,9 +136,11 @@ export function renderRoomChangeText(event) {
137
136
  case 'resume':
138
137
  return `${identity} lowered the hand`;
139
138
  case 'listen':
140
- return `${identity} turned an ear toward ${participantIdentity(event.target)}`;
139
+ return `${identity} turned an ear toward ${participantIdentity(event.target ?? 'someone')}`;
141
140
  case 'ignore':
142
- return `${identity} turned away from ${participantIdentity(event.target)}`;
141
+ return `${identity} turned away from ${participantIdentity(event.target ?? 'someone')}`;
142
+ default:
143
+ throw new Error('Expected a lifecycle activity');
143
144
  }
144
145
  }
145
146
  function renderedBody(body, maxChars) {
@@ -152,6 +153,12 @@ function bodySuffix(body) {
152
153
  return '';
153
154
  return `\n${body.split('\n').map((line) => ` ${line}`).join('\n')}`;
154
155
  }
156
+ function eventId(event) {
157
+ return 'index' in event ? formatActivityId(event.index) : event.id;
158
+ }
159
+ function replyId(reply) {
160
+ return typeof reply === 'number' ? formatActivityId(reply) : reply;
161
+ }
155
162
  function renderEventCliFacts(event, opts = {}) {
156
163
  const now = opts.now;
157
164
  const maxBody = opts.preview;
@@ -168,17 +175,17 @@ function renderEventCliFacts(event, opts = {}) {
168
175
  case 'say': {
169
176
  const body = renderedBody(event.body, maxBody);
170
177
  const mention = opts.mention;
171
- const mentionSuffix = mention !== undefined && audienceIncludes(audienceOf(event), mention)
178
+ const mentionSuffix = mention !== undefined && (event.reach === 'bell' || event.mentions?.some((name) => sameName(name, mention)))
172
179
  ? ` · calls your name across the square — ${participantIdentity(mention)}`
173
180
  : '';
174
- const replySuffix = event.reply === undefined ? '' : ` · replies to ${actId(event.reply)}`;
175
- const meta = ` #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, now)}${mentionSuffix}${replySuffix}`;
181
+ const replySuffix = event.reply === undefined ? '' : ` · replies to ${replyId(event.reply)}`;
182
+ const meta = ` #${opts.actNumber ?? 1} · ${eventId(event)} · ${formatRelativeTime(event.at, now)}${mentionSuffix}${replySuffix}`;
176
183
  return { text: `● ${participantIdentity(event.actor)}${style('dim', meta)}${bodySuffix(body.text)}`, bodyClipped: body.clipped };
177
184
  }
178
185
  case 'done': {
179
186
  const body = renderedBody(event.body, maxBody);
180
187
  return {
181
- text: `${style('dim', '○')} ${participantIdentity(event.actor)} stepped out of the square — done${style('dim', ` · ${actId(event)} · ${formatRelativeTime(event.at, now)}`)}${bodySuffix(body.text)}`,
188
+ text: `${style('dim', '○')} ${participantIdentity(event.actor)} stepped out of the square — done${style('dim', ` · ${eventId(event)} · ${formatRelativeTime(event.at, now)}`)}${bodySuffix(body.text)}`,
182
189
  bodyClipped: body.clipped,
183
190
  };
184
191
  }
@@ -190,9 +197,8 @@ export function renderEventCli(event, opts = {}) {
190
197
  return renderEventCliFacts(event, opts).text;
191
198
  }
192
199
  function renderPresenceOnlySay(event, opts = {}) {
193
- const audience = audienceOf(event);
194
- const targets = audience.kind === 'bell' ? [] : audience.names;
195
- const meta = style('dim', ` #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, opts.now)}`);
200
+ const targets = event.reach === 'bell' ? [] : event.mentions ?? [];
201
+ const meta = style('dim', ` #${opts.actNumber ?? 1} · ${eventId(event)} · ${formatRelativeTime(event.at, opts.now)}`);
196
202
  if (targets.length === 0) {
197
203
  return `● ${participantIdentity(event.actor)}${meta}`;
198
204
  }
@@ -207,10 +213,9 @@ function renderPresenceOnlySay(event, opts = {}) {
207
213
  export function renderAmbientEventFacts(event, viewer, opts = {}) {
208
214
  if (event.kind !== 'say')
209
215
  return renderEventCliFacts(event, opts);
210
- if (opts.perception === undefined && opts.squareState === undefined) {
211
- throw new Error('Ambient say rendering requires a settled perception or SquareState');
212
- }
213
- const seen = opts.perception ?? perceiveActivity(opts.squareState, event, viewer);
216
+ if (opts.perception === undefined)
217
+ throw new Error('Ambient say rendering requires a settled perception');
218
+ const seen = opts.perception;
214
219
  if (seen === 'presence') {
215
220
  const presence = renderPresenceOnlySay(event, opts);
216
221
  const body = opts.presencePreview === undefined ? { text: '', clipped: false } : renderedBody(event.body, opts.presencePreview);
@@ -248,17 +253,8 @@ function renderUnreadSummary(opts) {
248
253
  ...(remainingSummaries === 0 ? [] : [style('dim', ` · ${remainingSummaries} more participants have unread activity`)]),
249
254
  ];
250
255
  }
251
- export function renderPendingFeed(history, publicItems, viewer = '', squareState) {
252
- const lines = [];
253
- for (const act of publicItems) {
254
- const rendered = renderAmbientEvent(act, viewer, {
255
- actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
256
- ...(squareState === undefined ? {} : { squareState }),
257
- });
258
- if (rendered !== '')
259
- lines.push(rendered);
260
- }
261
- return lines.join('\n\n');
256
+ export function renderPendingFeed(publicItems, sayNumbers, perceptions, viewer) {
257
+ return publicItems.map((act) => renderAmbientEvent(act, viewer, { actNumber: sayNumbers[act.index], perception: perceptions[act.index] })).filter(Boolean).join('\n\n');
262
258
  }
263
259
  export function renderActivityBlocked(opts) {
264
260
  const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} catch`;
@@ -344,7 +340,7 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
344
340
  let truncated = false;
345
341
  for (const act of publicVisible) {
346
342
  const rawBody = act.body ?? '';
347
- const header = `${style('dim', actId(act.index))} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${style('dim', formatTimestamp(act.at))}`;
343
+ const header = `${style('dim', formatActivityId(act.index))} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${style('dim', formatTimestamp(act.at))}`;
348
344
  if (perception?.(act) === 'presence') {
349
345
  chunks.push(header);
350
346
  continue;
@@ -370,10 +366,10 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
370
366
  chunks.push(`${header}\n ${text.trim()}${omitted}`);
371
367
  }
372
368
  if (publicVisible.length === 1) {
373
- chunks.push(`${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2${truncated ? ' --no-truncate' : ''}`);
369
+ chunks.push(`${commandPrefix(squarePath)} history --at ${formatActivityId(publicVisible[0].index)} -C 2${truncated ? ' --no-truncate' : ''}`);
374
370
  }
375
371
  else if (truncated && publicVisible.length > 1) {
376
- chunks.push(`${commandPrefix(squarePath)} history --at ${actId(publicVisible[0].index)} -C 2 --no-truncate`);
372
+ chunks.push(`${commandPrefix(squarePath)} history --at ${formatActivityId(publicVisible[0].index)} -C 2 --no-truncate`);
377
373
  }
378
374
  return chunks.join('\n\n');
379
375
  }
@@ -447,20 +443,3 @@ export function renderDoctorClean() {
447
443
  export function renderDoctorUnfixable(reason) {
448
444
  return ['✕ unreadable artifact', ` · ${truncateExternalDiagnostic(reason)}`].join('\n');
449
445
  }
450
- export function renderWatchOutput(history, publicItems, opts) {
451
- const sections = [];
452
- if (publicItems.length > 0) {
453
- const rendered = publicItems
454
- .map((act) => renderAmbientEvent(act, opts.viewer, {
455
- actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
456
- mention: opts.mention,
457
- ...(opts.perceptions?.has(act.index) ? { perception: opts.perceptions.get(act.index) } : {}),
458
- ...(opts.squareState === undefined ? {} : { squareState: opts.squareState }),
459
- }))
460
- .filter(Boolean)
461
- .join('\n\n');
462
- if (rendered !== '')
463
- sections.push(rendered);
464
- }
465
- return sections.join('\n\n') + '\n';
466
- }
@@ -2,6 +2,8 @@
2
2
  import { type FileHostLedgerPort } from './host-ledger-file-adapter.js';
3
3
  import type { HostLedgerPort, PresenceChannel, PresenceRecord } from './host-ledger.js';
4
4
  export declare function presenceEpoch(record: PresenceRecord | undefined): number;
5
+ /** The current owner: the highest epoch wins, then the most recently updated row. */
6
+ export declare function currentOwner(rows: readonly PresenceRecord[]): PresenceRecord | undefined;
5
7
  export declare function localParticipantName(squarePath: string, hostLedger: HostLedgerPort, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
6
8
  export declare function squareAssignedParticipantName(env?: NodeJS.ProcessEnv): string | undefined;
7
9
  export type OwnershipClaim = {
package/dist/registry.js CHANGED
@@ -8,7 +8,7 @@ export function presenceEpoch(record) {
8
8
  return typeof record?.epoch === 'number' && Number.isSafeInteger(record.epoch) && record.epoch > 0 ? record.epoch : 0;
9
9
  }
10
10
  /** The current owner: the highest epoch wins, then the most recently updated row. */
11
- function currentOwner(rows) {
11
+ export function currentOwner(rows) {
12
12
  return [...rows].sort((left, right) => presenceEpoch(right) - presenceEpoch(left) || (right.updatedAt ?? 0) - (left.updatedAt ?? 0))[0];
13
13
  }
14
14
  export async function localParticipantName(squarePath, hostLedger, env = process.env) { const canonical = await canonicalPath(squarePath); const names = new Set((await Promise.all(localSessionIdentities(env).map(async (identity) => (await hostLedger.listPresence({ location: canonical, session: identity.sessionId })).map((row) => row.participant)))).flat()); return names.size === 1 ? [...names][0] : undefined; }
package/dist/routes.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type WakeRoute, type WakeRouteKind } from './model.js';
2
+ import type { SquareRead, SquareWrite } from './ports.js';
2
3
  export { WAKE_ROUTE_KINDS } from './model.js';
3
4
  export type { WakeRoute, WakeRouteKind } from './model.js';
4
5
  export declare const ROUTE_FRESH_MS: number;
@@ -18,25 +19,17 @@ export declare function routeIdentityKey(route: Pick<WakeRoute, 'location' | 'pa
18
19
  export type RouteEpoch = {
19
20
  readonly epoch?: number;
20
21
  };
21
- export declare function applyWakeRouteToState(state: import('./model.js').SquareState, route: Omit<WakeRoute, 'updatedAt'> & RouteEpoch, at?: number): void;
22
- export declare function dropSessionWakeRoutesFromState(state: import('./model.js').SquareState, location: string, sessionId: string, expectedEpoch?: number): void;
23
- /**
24
- * Drop one ended session's routes at a location, atomically with the lifecycle decide.
25
- *
26
- * A route survives when the ending transaction did not commit `done` and this session
27
- * still holds the sole live presence for its participant — an in-flight same-session
28
- * rejoin. `liveParticipants === undefined` means the ledger could not be read, so
29
- * nothing is dropped on suspicion. An explicit epoch fence additionally keeps routes
30
- * published by a later generation.
31
- */
32
- export declare function dropEndedSessionWakeRoutesFromState(state: import('./model.js').SquareState, location: string, sessionId: string, opts: {
22
+ export declare function applyWakeRoute(facts: SquareWrite, route: Omit<WakeRoute, 'updatedAt'> & RouteEpoch, at?: number): void;
23
+ export declare function dropSessionWakeRoutes(facts: SquareWrite, location: string, sessionId: string, expectedEpoch?: number): void;
24
+ /** Keep an in-flight same-session rejoin; unknown host evidence is not a reason to retire it. */
25
+ export declare function dropEndedSessionWakeRoutes(facts: SquareWrite, location: string, sessionId: string, opts: {
33
26
  readonly expectedEpoch?: number;
34
27
  readonly liveParticipants?: ReadonlySet<string>;
35
28
  readonly force: boolean;
36
29
  readonly participant?: string;
37
30
  }): void;
38
- export declare function dropParticipantWakeRoutesFromState(state: import('./model.js').SquareState, location: string, participant: string): void;
39
- export declare function sessionCanEndParticipant(state: import('./model.js').SquareState, location: string, participant: string, sessionId: string, currentSessionId?: string): boolean;
31
+ export declare function dropParticipantWakeRoutes(facts: SquareWrite, location: string, participant: string): void;
32
+ export declare function sessionCanEndParticipant(facts: SquareRead, location: string, participant: string, sessionId: string, sessionIdNow?: string): boolean;
40
33
  export declare function publishWakeRoute(artifact: import('./ports.js').SquareArtifactPort, route: Omit<WakeRoute, 'updatedAt'> & RouteEpoch, opts?: {
41
34
  at?: number;
42
35
  requireCurrentSession?: boolean;
package/dist/routes.js CHANGED
@@ -1,18 +1,8 @@
1
1
  import { nameKey } from './model.js';
2
- import { isCurrentlyJoined } from './runtime.js';
3
2
  import { canonicalPath, canonicalPathSync } from './canonical-path.js';
4
3
  export { WAKE_ROUTE_KINDS } from './model.js';
5
4
  export const ROUTE_FRESH_MS = 24 * 60 * 60 * 1000;
6
5
  export async function defaultWakeRouteCapabilities(hostLedger) {
7
- let userCapable = hostLedger !== undefined;
8
- if (hostLedger !== undefined) {
9
- try {
10
- await hostLedger.listPresence({ now: Date.now() });
11
- }
12
- catch {
13
- userCapable = false;
14
- }
15
- }
16
6
  const available = new Set();
17
7
  try {
18
8
  const { CodexQueueAdapter } = await import('./codex-queue.js');
@@ -24,7 +14,7 @@ export async function defaultWakeRouteCapabilities(hostLedger) {
24
14
  available.add(new PaseoAdapter().kind);
25
15
  }
26
16
  catch { /* optional */ }
27
- return { canUse: (kind, address) => userCapable && available.has(kind) && Object.values(address).every((value) => value.trim() !== '') };
17
+ return { canUse: (kind, address) => hostLedger !== undefined && available.has(kind) && Object.values(address).every((value) => value.trim() !== '') };
28
18
  }
29
19
  export function resolvePrimaryWakeRoute(boundary, env, capabilities) {
30
20
  const paseoAgentId = env.PASEO_AGENT_ID?.trim();
@@ -44,89 +34,61 @@ function sameRouteAddress(left, right) {
44
34
  const rightKeys = Object.keys(right).sort();
45
35
  return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => rightKeys[index] === key && left[key] === right[key]);
46
36
  }
47
- export function applyWakeRouteToState(state, route, at = Date.now()) {
37
+ export function applyWakeRoute(facts, route, at = Date.now()) {
48
38
  const location = canonicalPathSync(route.location);
49
- state.routes = [...(state.routes ?? []).filter((item) => {
50
- const itemLocation = canonicalPathSync(item.location);
51
- return !(itemLocation === location && routeIdentityKey(item, itemLocation) === routeIdentityKey(route, location));
52
- }), { ...route, location, updatedAt: at }];
39
+ const identity = routeIdentityKey(route, location);
40
+ facts.replaceRoutes([...facts.routes().filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) !== identity),
41
+ { ...route, location, updatedAt: at, extra: {} }]);
53
42
  }
54
- export function dropSessionWakeRoutesFromState(state, location, sessionId, expectedEpoch) {
43
+ export function dropSessionWakeRoutes(facts, location, sessionId, expectedEpoch) {
55
44
  const canonical = canonicalPathSync(location);
56
- state.routes = (state.routes ?? []).filter((item) => {
57
- if (item.sessionId !== sessionId || canonicalPathSync(item.location) !== canonical)
58
- return true;
59
- if (expectedEpoch === undefined)
60
- return false;
61
- return item.epoch !== expectedEpoch;
62
- });
45
+ facts.replaceRoutes(facts.routes().filter((item) => item.sessionId !== sessionId || canonicalPathSync(item.location) !== canonical
46
+ || (expectedEpoch !== undefined && item.epoch !== expectedEpoch)));
63
47
  }
64
- /**
65
- * Drop one ended session's routes at a location, atomically with the lifecycle decide.
66
- *
67
- * A route survives when the ending transaction did not commit `done` and this session
68
- * still holds the sole live presence for its participant — an in-flight same-session
69
- * rejoin. `liveParticipants === undefined` means the ledger could not be read, so
70
- * nothing is dropped on suspicion. An explicit epoch fence additionally keeps routes
71
- * published by a later generation.
72
- */
73
- export function dropEndedSessionWakeRoutesFromState(state, location, sessionId, opts) {
48
+ /** Keep an in-flight same-session rejoin; unknown host evidence is not a reason to retire it. */
49
+ export function dropEndedSessionWakeRoutes(facts, location, sessionId, opts) {
74
50
  const canonical = canonicalPathSync(location);
75
- state.routes = (state.routes ?? []).filter((item) => {
51
+ facts.replaceRoutes(facts.routes().filter((item) => {
76
52
  if (item.sessionId !== sessionId || canonicalPathSync(item.location) !== canonical)
77
53
  return true;
78
54
  if (opts.participant !== undefined && nameKey(item.participant) !== nameKey(opts.participant))
79
55
  return true;
80
- if (!opts.force) {
81
- // Keep only a route whose participant is still joined and whose ended session still
82
- // holds the sole live presence — an in-flight same-session rejoin. A no-longer-joined
83
- // participant is retired even when a stale presence row survives.
84
- if (isCurrentlyJoined(state.acts, item.participant)
85
- && (opts.liveParticipants === undefined || opts.liveParticipants.has(nameKey(item.participant))))
86
- return true;
87
- }
88
- if (opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch)
56
+ if (!opts.force && facts.participant(item.participant)?.joined
57
+ && (opts.liveParticipants === undefined || opts.liveParticipants.has(nameKey(item.participant))))
89
58
  return true;
90
- return false;
91
- });
59
+ return opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch;
60
+ }));
92
61
  }
93
- export function dropParticipantWakeRoutesFromState(state, location, participant) {
62
+ export function dropParticipantWakeRoutes(facts, location, participant) {
94
63
  const canonical = canonicalPathSync(location);
95
- state.routes = (state.routes ?? []).filter((item) => !(nameKey(item.participant) === nameKey(participant) && canonicalPathSync(item.location) === canonical));
64
+ facts.replaceRoutes(facts.routes().filter((item) => !(nameKey(item.participant) === nameKey(participant) && canonicalPathSync(item.location) === canonical)));
96
65
  }
97
- export function sessionCanEndParticipant(state, location, participant, sessionId, currentSessionId) {
66
+ export function sessionCanEndParticipant(facts, location, participant, sessionId, sessionIdNow) {
98
67
  const canonical = canonicalPathSync(location);
99
- const owned = (state.routes ?? []).filter((item) => nameKey(item.participant) === nameKey(participant) && canonicalPathSync(item.location) === canonical);
68
+ const owned = facts.routes().filter((item) => nameKey(item.participant) === nameKey(participant) && canonicalPathSync(item.location) === canonical);
100
69
  if (owned.length === 0)
101
- return currentSessionId === sessionId;
102
- return owned.every((item) => item.sessionId === sessionId) && (currentSessionId === undefined || currentSessionId === sessionId);
70
+ return sessionIdNow === sessionId;
71
+ return owned.every((item) => item.sessionId === sessionId) && (sessionIdNow === undefined || sessionIdNow === sessionId);
103
72
  }
104
73
  export async function publishWakeRoute(artifact, route, opts = {}) {
105
74
  const location = await canonicalPath(route.location);
106
75
  const at = opts.at ?? Date.now();
107
- await artifact.transact((state) => {
108
- if (opts.requireCurrentSession) {
109
- if (!isCurrentlyJoined(state.acts, route.participant)
110
- || (state.routes ?? []).some((item) => nameKey(item.participant) === nameKey(route.participant) && canonicalPathSync(item.location) === location && item.sessionId !== route.sessionId))
111
- return { state, result: undefined };
112
- }
113
- const identity = routeIdentityKey({ ...route, location });
114
- const matching = (state.routes ?? []).filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) === identity);
76
+ await artifact.transact((facts) => {
77
+ const routes = facts.routes();
78
+ if (opts.requireCurrentSession && (!facts.participant(route.participant)?.joined
79
+ || routes.some((item) => nameKey(item.participant) === nameKey(route.participant) && canonicalPathSync(item.location) === location && item.sessionId !== route.sessionId)))
80
+ return;
81
+ const identity = routeIdentityKey(route, location);
82
+ const matching = routes.filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) === identity);
115
83
  const existing = matching[0];
116
- const existingEpoch = existing?.epoch;
117
- const epochMatches = route.epoch === undefined ? existingEpoch === undefined : existingEpoch === route.epoch;
118
- if (matching.length === 1 && existing !== undefined && existing.updatedAt > at - ROUTE_FRESH_MS && existing.kind === route.kind && existing.channel === route.channel && sameRouteAddress(existing.address, route.address) && epochMatches)
119
- return { result: undefined };
120
- return { state: { ...state, routes: [...(state.routes ?? []).filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) !== identity), { ...route, location, updatedAt: at }] }, result: undefined };
84
+ if (matching.length === 1 && existing.updatedAt > at - ROUTE_FRESH_MS && existing.kind === route.kind
85
+ && existing.channel === route.channel && sameRouteAddress(existing.address, route.address) && existing.epoch === route.epoch)
86
+ return;
87
+ applyWakeRoute(facts, { ...route, location }, at);
121
88
  });
122
89
  }
123
90
  export async function retireWakeRouteFromArtifact(artifact, route, opts = {}) {
124
91
  const location = await canonicalPath(route.location);
125
- const target = routeIdentityKey({ ...route, location });
126
- await artifact.transact((state) => ({ state: { ...state, routes: (state.routes ?? []).filter((item) => {
127
- const itemLocation = canonicalPathSync(item.location);
128
- if (routeIdentityKey(item, itemLocation) !== target)
129
- return true;
130
- return opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch;
131
- }) }, result: undefined }));
92
+ const target = routeIdentityKey(route, location);
93
+ await artifact.transact((facts) => facts.replaceRoutes(facts.routes().filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) !== target || (opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch))));
132
94
  }