@astrosheep/square 0.3.93 → 0.3.94

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 (116) hide show
  1. package/dist/activity-feed.d.ts +3 -5
  2. package/dist/activity-feed.js +16 -14
  3. package/dist/artifact.js +2 -2
  4. package/dist/boundary-presentation.d.ts +2 -2
  5. package/dist/catch-decisions.d.ts +4 -6
  6. package/dist/catch-decisions.js +4 -4
  7. package/dist/claude-delivery-status.js +10 -4
  8. package/dist/claude-delivery.js +19 -11
  9. package/dist/claude-mod.js +24 -53
  10. package/dist/cli/context.d.ts +9 -4
  11. package/dist/cli/context.js +30 -25
  12. package/dist/cli/express-presentation.d.ts +3 -1
  13. package/dist/cli/express-presentation.js +26 -14
  14. package/dist/cli/json.d.ts +3 -0
  15. package/dist/cli/json.js +14 -0
  16. package/dist/cli/mcp-server-command.js +2 -2
  17. package/dist/cli/observation-commands.d.ts +1 -3
  18. package/dist/cli/observation-commands.js +43 -56
  19. package/dist/cli/program.js +2 -2
  20. package/dist/cli/registry.js +6 -3
  21. package/dist/cli/square-commands.d.ts +1 -1
  22. package/dist/cli/square-commands.js +46 -54
  23. package/dist/codex-hook.d.ts +3 -2
  24. package/dist/codex-hook.js +13 -4
  25. package/dist/codex-queue.js +4 -4
  26. package/dist/decisions.d.ts +5 -11
  27. package/dist/decisions.js +9 -20
  28. package/dist/delivery-deferral.js +2 -2
  29. package/dist/delivery-health.d.ts +0 -1
  30. package/dist/delivery-health.js +1 -3
  31. package/dist/delivery-operations.d.ts +2 -2
  32. package/dist/delivery-operations.js +38 -38
  33. package/dist/delivery.d.ts +1 -1
  34. package/dist/delivery.js +1 -1
  35. package/dist/help.js +12 -10
  36. package/dist/inbox.d.ts +9 -22
  37. package/dist/inbox.js +23 -27
  38. package/dist/index.d.ts +1 -3
  39. package/dist/index.js +1 -1
  40. package/dist/mcp.d.ts +3 -0
  41. package/dist/mcp.js +23 -24
  42. package/dist/model.d.ts +8 -15
  43. package/dist/model.js +4 -7
  44. package/dist/notifications.d.ts +1 -1
  45. package/dist/notifications.js +6 -5
  46. package/dist/operation-presentation.d.ts +5 -4
  47. package/dist/operation-presentation.js +50 -45
  48. package/dist/owner.d.ts +11 -0
  49. package/dist/owner.js +32 -0
  50. package/dist/paseo-delivery.d.ts +4 -1
  51. package/dist/paseo-delivery.js +7 -12
  52. package/dist/paseo-state.d.ts +2 -2
  53. package/dist/paseo-state.js +6 -4
  54. package/dist/plugins/opencode/delivery.js +27 -15
  55. package/dist/ports.d.ts +13 -33
  56. package/dist/presentation-operations.js +4 -4
  57. package/dist/presentation.d.ts +5 -8
  58. package/dist/presentation.js +33 -43
  59. package/dist/runtime.d.ts +1 -7
  60. package/dist/runtime.js +0 -14
  61. package/dist/seats.d.ts +2 -3
  62. package/dist/seats.js +7 -15
  63. package/dist/session-lifecycle.d.ts +6 -2
  64. package/dist/session-lifecycle.js +39 -11
  65. package/dist/sqlite-square.d.ts +36 -22
  66. package/dist/sqlite-square.js +138 -161
  67. package/dist/square-api.d.ts +15 -10
  68. package/dist/square-api.js +26 -22
  69. package/dist/square-core.d.ts +0 -5
  70. package/dist/square-core.js +0 -9
  71. package/dist/square-domain.d.ts +4 -2
  72. package/dist/square-domain.js +56 -21
  73. package/dist/square-facade.d.ts +45 -87
  74. package/dist/square-index.d.ts +5 -13
  75. package/dist/square-index.js +13 -38
  76. package/dist/square-location.d.ts +1 -14
  77. package/dist/square-location.js +4 -70
  78. package/dist/square-migration-host-ledger.d.ts +2 -2
  79. package/dist/square-migration-host-ledger.js +6 -5
  80. package/dist/square-migration.d.ts +2 -1
  81. package/dist/square-migration.js +52 -30
  82. package/dist/square-sqlite.d.ts +6 -2
  83. package/dist/square-sqlite.js +54 -28
  84. package/dist/square-tools.d.ts +7 -10
  85. package/dist/square-tools.js +22 -54
  86. package/dist/stream.js +4 -5
  87. package/dist/views.d.ts +21 -10
  88. package/dist/views.js +38 -38
  89. package/dist/wakes.d.ts +1 -2
  90. package/dist/wakes.js +1 -1
  91. package/guides/application-facade-delivery-evidence.md +1 -1
  92. package/guides/claude-native.md +18 -9
  93. package/guides/codex-delivery.md +17 -6
  94. package/guides/mcp-connection-lifetime.md +1 -1
  95. package/guides/mcp.md +3 -3
  96. package/guides/migration.md +142 -151
  97. package/guides/operation-cancellation.md +1 -1
  98. package/guides/participant-ownership.md +78 -47
  99. package/guides/pi-tools.md +33 -4
  100. package/guides/primitive-history-consolidation.md +1 -1
  101. package/guides/row-storage-design.md +8 -8
  102. package/guides/square-api.md +244 -88
  103. package/package.json +1 -1
  104. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  105. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  106. package/plugins/opencode/delivery.ts +27 -17
  107. package/plugins/paseo/client/palette.ts +18 -18
  108. package/plugins/paseo/client/square-message.tsx +17 -23
  109. package/plugins/pi/square-pi.js +17 -17
  110. package/dist/square-backend.d.ts +0 -13
  111. package/dist/square-backend.js +0 -1
  112. package/dist/square-stdio.d.ts +0 -22
  113. package/dist/square-stdio.js +0 -235
  114. package/dist/stdio-server.d.ts +0 -3
  115. package/dist/stdio-server.js +0 -212
  116. package/guides/backends.md +0 -193
@@ -1,26 +1,24 @@
1
1
  // Human-facing text for one settled operation. The CLI and the MCP transport both
2
2
  // render through these functions, so a person sees the same square either way.
3
3
  import { sameName } from './model.js';
4
- import { formatActivityId, parseActivityId } from './square-core.js';
5
- import { commandPrefix, joinRecoveryCommand, participantCommandPrefix, participantIdentity, participantsRecoveryCommand, quoteShell, renderAmbientEvent, renderAmbientEventFacts, renderEventCli, renderGrepActivitiesView, renderPendingFeed, renderPresenceAnchor, renderRoomChangeText, renderWatchAlreadyActive, renderWatchReplaced, renderWatchStatus, takeoverRecoveryLines, withPathOutput, } from './presentation.js';
4
+ import { formatActivityId } from './square-core.js';
5
+ import { commandPrefix, joinRecoveryCommand, participantCommandPrefix, participantIdentity, participantsRecoveryCommand, quoteShell, renderAmbientEvent, renderAmbientEventFacts, renderEventCli, renderGrepActivitiesView, renderPresenceAnchor, renderRoomChangeText, renderWatchAlreadyActive, renderWatchReplaced, renderWatchStatus, takeoverRecoveryLines, withPathOutput, } from './presentation.js';
6
6
  import { formatRelativeTime, formatTimestamp } from './time.js';
7
7
  import { style } from './tty-style.js';
8
8
  export const PARTICIPANTS_DEFAULT_LIMIT = 20;
9
9
  export const PARTICIPANTS_MAX_LIMIT = 100;
10
10
  const STATUS_PARTICIPANT_PREVIEW_LIMIT = 10;
11
- export function renderJoinResult(data) {
11
+ export function renderJoinResult(data, owner) {
12
12
  const after = data.observation;
13
13
  if (after === undefined)
14
14
  return undefined;
15
15
  const entry = after.entry;
16
16
  const name = typeof data.participant === 'string' ? data.participant : data.participant.name;
17
17
  const activities = (entry?.recentActivities ?? []).map((event) => renderAmbientEvent(event, name, {
18
- now: after.now, preview: 200,
19
- actNumber: event.kind === 'say' ? entry.sayNumbers[event.index] : undefined,
20
- perception: entry.perceptions[event.index],
18
+ now: after.now, preview: 200, perception: event.perception,
21
19
  })).filter(Boolean).join('\n\n');
22
- const fallback = after.automaticDeliveryAvailable ? ['', '· when someone calls you, you’ll hear them here'] : [
23
- '', `${participantCommandPrefix(after.location, name)} catch --idle 30m`,
20
+ const fallback = after.deliveryAvailable ? ['', '· when someone calls you, you’ll hear them here'] : [
21
+ '', `${participantCommandPrefix(after.location, name, owner)} catch --idle 30m`,
24
22
  ' · stay within earshot — keep this catch open for new activity',
25
23
  ];
26
24
  const entryLine = data.kind === 'joined' ? '● you stepped into the square'
@@ -41,16 +39,18 @@ export function renderJoinResult(data) {
41
39
  function isGestureOnly(body) {
42
40
  return /\*[^*]*\*/.test(body) && body.replace(/\*[^*]*\*/g, '').trim() === '';
43
41
  }
44
- export function renderExpression(squarePath, data) {
42
+ export function renderExpression(squarePath, data, owner) {
45
43
  const projection = data.observation;
46
44
  if (projection === undefined)
47
45
  return undefined;
48
46
  const name = projection.participant;
49
- const pendingSpeaker = projection.pendingPublic[0]?.actor;
50
- const pendingLead = pendingSpeaker === undefined ? ''
51
- : `▲ while you spoke, ${participantIdentity(pendingSpeaker)} said something behind you\n\n`;
52
- const pending = projection.pendingPublic.length === 0 ? ''
53
- : `\n\n${pendingLead}${renderPendingFeed(projection.pendingPublic, projection.sayNumbers, projection.perceptions, name)}`;
47
+ const context = projection.context;
48
+ const pending = context.activities.length === 0 ? '' : '\n\n' + [
49
+ 'Unread context · preview only, not consumed',
50
+ ...context.activities.map(item => `· ${participantIdentity(item.actor)} · ${item.id}\n ${item.preview}${item.truncated ? '… (truncated)' : ''}`),
51
+ ...(context.remaining === 0 ? [] : [`· ${context.remaining} more unread activities`]),
52
+ `${participantCommandPrefix(projection.location, name, owner)} catch`,
53
+ ].join('\n');
54
54
  const ownCount = projection.ownActivityCount;
55
55
  const capCount = projection.hardCap === null ? '' : ` · ${ownCount}/${projection.hardCap}`;
56
56
  const activity = data.activity;
@@ -65,7 +65,7 @@ export function renderExpression(squarePath, data) {
65
65
  ? `\n · ${data.unmatchedMentions.map((token) => `@${token} isn't standing here`).join('; ')} — ${activity.mentions.length === 0 && activity.reply === undefined ? 'activity landed bare' : 'left as literal text'}` : '';
66
66
  return withPathOutput(projection.location ?? squarePath, confirmation + (data.hint ? `\n${style('dim', `· ${data.hint}`)}` : '') + replyNotice + unmatchedNotice + pending, { participantCount: projection.standingCount, held: projection.held });
67
67
  }
68
- function continuationCommand(squarePath, name, options) {
68
+ function continuationCommand(squarePath, name, options, owner) {
69
69
  const args = ['--now'];
70
70
  if (options.from?.length)
71
71
  args.push('--from', options.from.join(','));
@@ -73,20 +73,20 @@ function continuationCommand(squarePath, name, options) {
73
73
  args.push('--mention');
74
74
  if (options.limit !== undefined)
75
75
  args.push('--limit', String(options.limit));
76
- return `${participantCommandPrefix(squarePath, name)} catch ${args.join(' ')}`;
76
+ return `${participantCommandPrefix(squarePath, name, owner)} catch ${args.join(' ')}`;
77
77
  }
78
78
  /** Render settled catch facts; no selection, terminal decisions, receipts or wait machinery. */
79
- export function renderCatchOutcome(squarePath, data, options = {}) {
79
+ export function renderCatchOutcome(squarePath, data, options = {}, owner) {
80
80
  const projection = data.observation;
81
81
  if (projection === undefined)
82
82
  return { stdout: '', exitCode: 0 };
83
83
  const location = projection.location ?? squarePath;
84
84
  const name = projection.participant;
85
- const stop = projection.stop ?? data.status;
85
+ const stop = data.reason === 'drained' && data.activities.length > 0 ? undefined : data.reason;
86
86
  const header = { participantCount: projection.standingCount, held: projection.held };
87
87
  const statusOptions = {
88
- squarePath: location, name, presence: projection.presence,
89
- showCatchHint: !projection.automaticDeliveryAvailable,
88
+ squarePath: location, name, owner, presence: projection.presence,
89
+ showCatchHint: !projection.deliveryAvailable,
90
90
  ownActivityCount: projection.ownActivityCount, hardCap: projection.hardCap,
91
91
  };
92
92
  if (stop === 'already-active' || stop === 'replaced') {
@@ -97,17 +97,16 @@ export function renderCatchOutcome(squarePath, data, options = {}) {
97
97
  };
98
98
  }
99
99
  const rendered = data.activities.map((activity) => renderAmbientEvent(activity, name, {
100
- now: projection.now, actNumber: projection.sayNumbers[parseActivityId(activity.id)],
101
- perception: activity.perception, mention: options.mention ? name : undefined,
100
+ now: projection.now, perception: activity.perception, mention: options.mention ? name : undefined,
102
101
  })).filter(Boolean).join('\n\n');
103
102
  const remaining = data.remaining === 0 ? ''
104
- : `· ${data.remaining} matching ${data.remaining === 1 ? 'activity remains' : 'activities remain'}\n${continuationCommand(location, name, options)}`;
103
+ : `· ${data.remaining} matching ${data.remaining === 1 ? 'activity remains' : 'activities remain'}\n${continuationCommand(location, name, options, owner)}`;
105
104
  const terminal = stop === undefined ? '' : renderWatchStatus({
106
105
  ...statusOptions, status: stop,
107
- ...(stop === 'stale' && options.idle !== undefined ? { idleMs: options.idle } : {}),
106
+ ...(stop === 'idle' && options.idle !== undefined ? { idleMs: options.idle } : {}),
108
107
  });
109
- const fallback = stop === undefined && !projection.automaticDeliveryAvailable && data.remaining === 0
110
- ? `${participantCommandPrefix(location, name)} catch --idle 30m\n stay available for new activity` : '';
108
+ const fallback = stop === undefined && !projection.deliveryAvailable && data.remaining === 0
109
+ ? `${participantCommandPrefix(location, name, owner)} catch --idle 30m\n stay available for new activity` : '';
111
110
  const hint = data.hint === undefined ? '' : style('dim', `· ${data.hint}`);
112
111
  return {
113
112
  stdout: withPathOutput(location, [terminal, rendered, hint, remaining, fallback].filter(Boolean).join('\n\n'), header),
@@ -121,11 +120,7 @@ function renderHistoryProjection(projection, visible, noTruncate, squarePath) {
121
120
  // Presence anchors collect into one closing line instead of interrupting the page.
122
121
  const anchored = [];
123
122
  for (const activity of shown) {
124
- const options = {
125
- preview,
126
- actNumber: activity.kind === 'say' ? projection.sayNumbers[activity.index] : undefined,
127
- };
128
- const rendered = renderEventCli(activity, options);
123
+ const rendered = renderEventCli(activity, { preview });
129
124
  if (rendered !== '')
130
125
  chunks.push(rendered);
131
126
  for (const participant of projection.presenceAnchors[activity.index] ?? []) {
@@ -193,8 +188,8 @@ export function renderParticipantsResult(squarePath, data, limit = PARTICIPANTS_
193
188
  const now = projection.now;
194
189
  const participants = data.participants;
195
190
  const lines = participants.slice(0, limit).map((participant) => {
196
- const glyph = participant.state === 'done' ? '○' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 && projection.classificationByName[participant.name] === 'here' ? '●' : '○';
197
- const state = participant.state === 'done' ? 'done' : participant.presence === 'watching' ? 'catching' : participant.state;
191
+ const glyph = participant.presence === 'done' ? '○' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 && projection.classificationByName[participant.name] === 'here' ? '●' : '○';
192
+ const state = participant.presence === 'watching' ? 'catching' : participant.presence === 'never-joined' ? 'not joined' : participant.presence;
198
193
  const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, now);
199
194
  return ` ${glyph} ${participant.name}${style('dim', ` · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`)}`;
200
195
  });
@@ -217,8 +212,9 @@ export function renderStatusResult(squarePath, data) {
217
212
  const participantsByName = new Map(data.participants.map((participant) => [participant.name, participant]));
218
213
  const orderedHere = presentation.orderedHereNames.map((name) => participantsByName.get(name));
219
214
  const lingering = presentation.lingeringCount;
215
+ const away = data.participants.filter(participant => participant.presence === 'away');
220
216
  const people = [];
221
- if (data.activeCount === 0) {
217
+ if (presentation.standingCount === 0) {
222
218
  people.push(' ○ nobody in the square');
223
219
  }
224
220
  else {
@@ -243,7 +239,11 @@ export function renderStatusResult(squarePath, data) {
243
239
  : 'caught up';
244
240
  return ` ${glyph} ${participantIdentity(participant.name)}${style('dim', ` ${summary}${attention === '' ? '' : ` · ${attention}`}`)}`;
245
241
  }));
246
- const unshown = Math.max(0, orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT) + lingering;
242
+ people.push(...away.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map(participant => ` ○ ${participantIdentity(participant.name)}${style('dim', ' · away')}`));
243
+ if (away.length > STATUS_PARTICIPANT_PREVIEW_LIMIT)
244
+ people.push(` ○ … ${away.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more away`);
245
+ const unshown = Math.max(0, orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT)
246
+ + Math.max(0, away.length - STATUS_PARTICIPANT_PREVIEW_LIMIT) + lingering;
247
247
  if (orderedHere.length > STATUS_PARTICIPANT_PREVIEW_LIMIT) {
248
248
  people.push(` ○ … ${orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more here`);
249
249
  }
@@ -256,16 +256,16 @@ export function renderStatusResult(squarePath, data) {
256
256
  }
257
257
  }
258
258
  const cap = data.hardCap === null ? 'unlimited' : `${data.hardCap} each`;
259
- const holdTime = data.hold?.at === undefined ? 'just now' : formatRelativeTime(data.hold.at, data.now);
260
- const hold = data.hold !== null
261
- ? `· ${data.hold.by === undefined ? 'someone' : participantIdentity(data.hold.by)} raised a hand${data.hold.reason ? ` — ${data.hold.reason}` : ''}${style('dim', ` · ${holdTime}`)}`
259
+ const held = data.held;
260
+ const holdTime = held?.at === undefined ? 'just now' : formatRelativeTime(held.at, data.now);
261
+ const hold = held !== null
262
+ ? `· ${held.by === undefined ? 'someone' : participantIdentity(held.by)} raised a hand${held.reason ? ` — ${held.reason}` : ''}${style('dim', ` · ${holdTime}`)}`
262
263
  : undefined;
263
264
  const visible = data.latestActivity === undefined
264
265
  ? { text: '', bodyClipped: false }
265
266
  : renderAmbientEventFacts(data.latestActivity, presentation.viewer, {
266
267
  now: data.now,
267
268
  preview: 200,
268
- actNumber: presentation.latestActNumber,
269
269
  perception: presentation.latestPerception,
270
270
  });
271
271
  const latest = visible.text === ''
@@ -285,8 +285,9 @@ export function renderStatusResult(squarePath, data) {
285
285
  }
286
286
  const counts = [
287
287
  `${orderedHere.length} here`,
288
+ ...(away.length > 0 ? [`${away.length} away`] : []),
288
289
  ...(lingering > 0 ? [`${lingering} lingering`] : []),
289
- `${data.doneCount} done`,
290
+ `${data.participants.filter((participant) => participant.presence === 'done').length} done`,
290
291
  `cap ${cap}`,
291
292
  `throttle ${data.throttlePerMinute === undefined ? 'none' : `${data.throttlePerMinute}/min`}`,
292
293
  ].join(' · ');
@@ -298,7 +299,7 @@ export function renderStatusResult(squarePath, data) {
298
299
  ...(data.listening.length === 0 ? [' ○ not turned toward anyone'] : data.listening.map((name) => ` · ${participantIdentity(name)}`)),
299
300
  ]),
300
301
  ].join('\n');
301
- return withPathOutput(squarePath, output, { participantCount: presentation.standingCount, held: presentation.held });
302
+ return withPathOutput(squarePath, output, { participantCount: presentation.standingCount, held: data.held !== null });
302
303
  }
303
304
  export function renderListeningResult(squarePath, data) {
304
305
  const projection = data.observation;
@@ -334,12 +335,12 @@ export function renderLifecycleResult(squarePath, verb, data) {
334
335
  if (verb === 'done') {
335
336
  return withPathOutput(projection.location ?? squarePath, `○ you stepped out — your footsteps fade · ${data.activity.id}`, header);
336
337
  }
337
- const line = `${renderRoomChangeText(projection.activity)} · ${formatActivityId(projection.activity.index)} · just now`;
338
+ const line = `${renderRoomChangeText(data.activity)} · ${data.activity.id} · just now`;
338
339
  return withPathOutput(projection.location ?? squarePath, verb === 'hold' ? `· ${line}` : `✓ ${line}`, header);
339
340
  }
340
341
  /** Refusal lines with the same recovery commands the CLI prints. */
341
342
  export function renderFailureLines(failure, options = {}) {
342
- const { squarePath, name } = options;
343
+ const { squarePath, name, owner } = options;
343
344
  const facts = failure.facts;
344
345
  const reason = facts?.reason;
345
346
  const marked = reason === 'never_joined' || reason === 'not_standing';
@@ -350,13 +351,17 @@ export function renderFailureLines(failure, options = {}) {
350
351
  else if (squarePath !== undefined)
351
352
  lines.push(`${commandPrefix(squarePath)} migrate`);
352
353
  }
354
+ else if (squarePath !== undefined && name !== undefined && reason === 'needs_seat') {
355
+ lines.splice(0, 1, `✕ ${failure.message}`);
356
+ lines.push(joinRecoveryCommand(squarePath, name, owner));
357
+ }
353
358
  else if (squarePath !== undefined && name !== undefined && failure.code === 'already_joined') {
354
359
  lines.splice(0, 1, `✕ ${participantIdentity(name)} shoos you out of the square`, ` · ${participantIdentity(name)} already stands here — another session holds the name`);
355
- lines.push(...takeoverRecoveryLines(squarePath, name));
360
+ lines.push(...takeoverRecoveryLines(squarePath, name, owner));
356
361
  }
357
362
  else if (squarePath !== undefined && reason === 'never_joined') {
358
363
  lines.push(failure.operation?.subject !== 'filter' && name !== undefined
359
- ? joinRecoveryCommand(squarePath, name) : participantsRecoveryCommand(squarePath));
364
+ ? joinRecoveryCommand(squarePath, name, owner) : participantsRecoveryCommand(squarePath));
360
365
  }
361
366
  else if (squarePath !== undefined && reason === 'not_standing') {
362
367
  lines.push(participantsRecoveryCommand(squarePath));
@@ -0,0 +1,11 @@
1
+ import { type Owner } from './model.js';
2
+ export type { Owner } from './model.js';
3
+ export interface HarnessCoordinate {
4
+ readonly channel: string;
5
+ readonly session: string;
6
+ readonly agent?: string;
7
+ }
8
+ export declare function harnessOwner(coordinate: HarnessCoordinate): Owner;
9
+ /** Decode only at harness adapter boundaries; core never consumes the coordinate. */
10
+ export declare function decodeHarnessOwner(owner: Owner): HarnessCoordinate | undefined;
11
+ export declare function localUserOwner(): Owner;
package/dist/owner.js ADDED
@@ -0,0 +1,32 @@
1
+ import os from 'node:os';
2
+ import { validateOwner } from './model.js';
3
+ export function harnessOwner(coordinate) {
4
+ const channel = nonempty(coordinate.channel, 'channel');
5
+ const session = nonempty(coordinate.session, 'session');
6
+ const agent = coordinate.agent === undefined ? undefined : nonempty(coordinate.agent, 'agent');
7
+ return validateOwner([channel, session, ...(agent === undefined ? [] : ['agent', agent])].map(encodeURIComponent).join(':'));
8
+ }
9
+ /** Decode only at harness adapter boundaries; core never consumes the coordinate. */
10
+ export function decodeHarnessOwner(owner) {
11
+ const parts = owner.split(':');
12
+ if ((parts.length !== 2 && parts.length !== 4) || (parts.length === 4 && parts[2] !== 'agent'))
13
+ return undefined;
14
+ try {
15
+ const [channel, session] = parts;
16
+ const decoded = { channel: decodeURIComponent(channel), session: decodeURIComponent(session) };
17
+ if (parts.length === 4)
18
+ return { ...decoded, agent: decodeURIComponent(parts[3]) };
19
+ return decoded;
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ }
25
+ export function localUserOwner() {
26
+ return validateOwner(`user:${os.userInfo().username}@${os.hostname()}`);
27
+ }
28
+ function nonempty(value, label) {
29
+ if (typeof value !== 'string' || value.length === 0)
30
+ throw new Error(`Harness ${label} must be non-empty.`);
31
+ return value;
32
+ }
@@ -2,12 +2,15 @@ import type { WakeRequest, WakeOutcome, WakeTransportPort } from './ports.js';
2
2
  import { discoverPaseoAgents, waitForPaseoWakeBoundary } from './paseo-state.js';
3
3
  import { sendPaseoWake } from './wake-sink.js';
4
4
  export interface PaseoWakeTransportOptions {
5
- discover?: typeof discoverPaseoAgents;
5
+ discover?: (timeoutMs: number) => ReturnType<typeof discoverPaseoAgents> | Awaited<ReturnType<typeof discoverPaseoAgents>>;
6
6
  waitForBoundary?: typeof waitForPaseoWakeBoundary;
7
7
  sendWake?: typeof sendPaseoWake;
8
8
  }
9
9
  export declare class PaseoWakeTransport implements WakeTransportPort {
10
10
  private readonly opts;
11
+ private discovering?;
11
12
  constructor(opts?: PaseoWakeTransportOptions);
13
+ /** Coalesce overlapping inventory reads only; a later send must rediscover current status. */
14
+ private discover;
12
15
  attempt(request: WakeRequest, timeoutMs: number, beforeSend: () => Promise<boolean>): Promise<WakeOutcome>;
13
16
  }
@@ -17,28 +17,30 @@ function diagnostic(phase, address, code) {
17
17
  }
18
18
  export class PaseoWakeTransport {
19
19
  opts;
20
+ discovering;
20
21
  constructor(opts = {}) {
21
22
  this.opts = opts;
22
23
  }
24
+ /** Coalesce overlapping inventory reads only; a later send must rediscover current status. */
25
+ discover(timeoutMs) {
26
+ return this.discovering ??= Promise.resolve().then(() => (this.opts.discover ?? discoverPaseoAgents)(timeoutMs))
27
+ .finally(() => { this.discovering = undefined; });
28
+ }
23
29
  async attempt(request, timeoutMs, beforeSend) {
24
30
  const address = request.route.address;
25
31
  const deadline = Date.now() + timeoutMs;
26
32
  const remainingMs = () => Math.max(0, deadline - Date.now());
27
33
  const budgetUnavailable = () => ({
28
34
  outcome: 'failed',
29
- unavailable: true,
30
35
  signature: 'dispatch_budget_exhausted',
31
36
  message: 'The wake dispatch budget elapsed before Paseo accepted the wake.',
32
- retainRoute: true,
33
37
  });
34
38
  const agentId = address.agentId?.trim();
35
39
  if (!agentId) {
36
40
  return {
37
41
  outcome: 'failed',
38
- unavailable: true,
39
42
  signature: 'invalid_address',
40
43
  message: 'Paseo route has no agent id.',
41
- routeStale: true,
42
44
  diagnostic: diagnostic('selection', address, 'invalid_address'),
43
45
  };
44
46
  }
@@ -49,30 +51,25 @@ export class PaseoWakeTransport {
49
51
  let remaining = remainingMs();
50
52
  if (remaining === 0)
51
53
  return budgetUnavailable();
52
- const discovery = (this.opts.discover ?? discoverPaseoAgents)(remaining);
54
+ const discovery = await this.discover(remaining);
53
55
  if (discovery.error && discovery.agents.length === 0) {
54
56
  return {
55
57
  outcome: 'failed',
56
- unavailable: true,
57
58
  // A credential or authorization refusal is proven; anything else may be transient.
58
59
  signature: /password|auth|unauthori[sz]ed/i.test(discovery.error) ? 'discovery_rejected'
59
60
  : /DAEMON_NOT_RUNNING|ECONNREFUSED|ENOENT|not found.*executable|ETIMEDOUT|timed out|timeout/i.test(discovery.error)
60
61
  ? 'discovery_transient' : 'discovery_rejected',
61
62
  message: `Paseo unavailable: ${discovery.error}`,
62
63
  diagnostic: diagnostic('discovery', address, 'unavailable'),
63
- retainRoute: true,
64
64
  };
65
65
  }
66
66
  const agent = discovery.agents.find((candidate) => candidate.id === agentId);
67
67
  if (agent === undefined || agent.status !== 'idle') {
68
68
  return {
69
69
  outcome: 'failed',
70
- unavailable: true,
71
70
  signature: agent === undefined ? 'address_not_found' : 'agent_not_idle',
72
71
  message: agent === undefined ? 'The registered Paseo agent was not found.' : 'The registered Paseo agent is not idle.',
73
72
  diagnostic: diagnostic('selection', address, agent === undefined ? 'not_found' : 'not_idle'),
74
- ...(agent === undefined ? {} : { retainRoute: true }),
75
- ...(agent === undefined ? { routeStale: true } : {}),
76
73
  };
77
74
  }
78
75
  remaining = remainingMs();
@@ -81,11 +78,9 @@ export class PaseoWakeTransport {
81
78
  if (!(await (this.opts.waitForBoundary ?? waitForPaseoWakeBoundary)(agent, remaining))) {
82
79
  return {
83
80
  outcome: 'failed',
84
- unavailable: true,
85
81
  signature: 'boundary_unavailable',
86
82
  message: 'Paseo did not reach the current tool boundary before the wake timeout.',
87
83
  diagnostic: diagnostic('boundary', address, 'unavailable'),
88
- retainRoute: true,
89
84
  };
90
85
  }
91
86
  if (!(await beforeSend()))
@@ -7,8 +7,8 @@ export interface PaseoAgent {
7
7
  export declare function discoverPaseoAgents(timeoutMs?: number, opts?: {
8
8
  args?: string[];
9
9
  bin?: string;
10
- }): {
10
+ }): Promise<{
11
11
  agents: PaseoAgent[];
12
12
  error?: string;
13
- };
13
+ }>;
14
14
  export declare function waitForPaseoWakeBoundary(agent: Pick<PaseoAgent, 'id' | 'status'>, timeoutMs?: number): Promise<boolean>;
@@ -1,11 +1,13 @@
1
- import { execFileSync } from 'node:child_process';
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
2
3
  import { waitForPaseoToolBoundary } from './paseo-timeline.js';
3
- export function discoverPaseoAgents(timeoutMs = 5000, opts = {}) {
4
+ const execFileAsync = promisify(execFile);
5
+ export async function discoverPaseoAgents(timeoutMs = 5000, opts = {}) {
4
6
  try {
5
- const raw = execFileSync(opts.bin ?? process.env.SQUARE_PASEO_BIN ?? 'paseo', [...(opts.args ?? []), 'ls', '--global', '--json'], {
7
+ const { stdout: raw } = await execFileAsync(opts.bin ?? process.env.SQUARE_PASEO_BIN ?? 'paseo', [...(opts.args ?? []), 'ls', '--global', '--json'], {
6
8
  encoding: 'utf8',
7
9
  timeout: timeoutMs,
8
- stdio: ['ignore', 'pipe', 'pipe'],
10
+ maxBuffer: 1024 * 1024,
9
11
  });
10
12
  const parsed = JSON.parse(raw);
11
13
  const agents = Array.isArray(parsed) ? parsed : parsed?.agents;
@@ -3,39 +3,48 @@ import { createArtifactPort } from '../../artifact.js';
3
3
  import { deliverPending } from '../../delivery-operations.js';
4
4
  import { nativePendingPreview, observeSessionPending, projectSessionSeats } from '../../inbox.js';
5
5
  import { automaticWakeEligible, presentationBlocksWake } from '../../delivery.js';
6
- import { sameBody } from '../../model.js';
6
+ import { harnessOwner, decodeHarnessOwner } from '../../owner.js';
7
7
  import { formatActivityId, parseActivityId } from '../../square-core.js';
8
8
  import { indexedSquareEntries } from '../../square-index.js';
9
9
  import { asSeatRef } from '../../seats.js';
10
+ import { restoreSessionSeats } from '../../session-lifecycle.js';
10
11
  import { presentPending } from '../../presentation-operations.js';
11
12
  import { createNativeInputId, sendNativeText } from '../../packages/agent-delivery/src/opencode-native.js';
12
13
  const routeKind = 'opencode-server';
13
- const bodyFor = (sessionId) => ({ channel: 'opencode', session: sessionId });
14
+ const ownerFor = (sessionId) => harnessOwner({ channel: 'opencode', session: sessionId });
14
15
  const payloadHash = (payload) => createHash('sha256').update(payload).digest('hex');
15
- function preview(facts, location, seat, index) {
16
+ function preview(facts, location, captured, index) {
17
+ const seat = facts.seat(captured.participant);
18
+ if (seat === undefined || seat.attachment !== 'attached' || seat.generation !== captured.generation || seat.owner !== captured.owner)
19
+ return undefined;
20
+ const wake = seat.wakeAddress;
21
+ if (wake?.kind !== routeKind)
22
+ return undefined;
16
23
  return nativePendingPreview(facts, {
17
- location, routeKind, sessionId: seat.body.session, participant: seat.participant,
18
- actIndex: index, address: { sessionId: seat.body.session }, now: Date.now(),
24
+ location, routeKind, generation: seat.generation, participant: seat.participant,
25
+ actIndex: index, address: wake.address, now: Date.now(),
19
26
  });
20
27
  }
21
28
  export async function indexedOpenCodeSessions(env) {
22
- return [...new Set((await indexedSquareEntries({ channel: 'opencode' }, env)).map((entry) => entry.body.session))];
29
+ return [...new Set((await indexedSquareEntries({}, env)).flatMap((entry) => {
30
+ const coordinate = decodeHarnessOwner(entry.owner);
31
+ return coordinate?.channel === 'opencode' ? [coordinate.session] : [];
32
+ }))];
23
33
  }
24
34
  export async function discoverOpenCodeSeats(sessionId, env, signal) {
25
- return projectSessionSeats({ body: bodyFor(sessionId), env }, signal);
35
+ return projectSessionSeats({ owner: ownerFor(sessionId), env }, signal);
26
36
  }
27
37
  function sameSeat(left, right) {
28
- return left !== undefined && left.generation === right.generation && sameBody(left.body, right.body);
38
+ return left !== undefined && left.attachment === 'attached' && left.generation === right.generation && left.owner === right.owner;
29
39
  }
30
40
  function createOpenCodeWakeTransport(target, lifetime) {
31
41
  return {
32
42
  probe: async (route) => !lifetime.aborted && route.kind === routeKind
33
- && route.sessionId === target.sessionId && route.address.sessionId === target.sessionId,
43
+ && route.address.sessionId === target.sessionId,
34
44
  async attempt(request, timeoutMs, beforeSend) {
35
45
  const index = parseActivityId(request.activity);
36
46
  const artifact = request.artifact;
37
47
  if (artifact === undefined || request.claimToken === undefined || index === undefined
38
- || !sameBody(request.seat.body, bodyFor(target.sessionId))
39
48
  || request.route.kind !== routeKind || request.route.address.sessionId !== target.sessionId) {
40
49
  return { outcome: 'not-capable', diagnostic: 'OpenCode attention has no matching artifact claim or session.' };
41
50
  }
@@ -61,7 +70,7 @@ function createOpenCodeWakeTransport(target, lifetime) {
61
70
  if (result.state === 'accepted')
62
71
  return { outcome: 'accepted' };
63
72
  if (result.state === 'unavailable')
64
- return { outcome: 'failed', unavailable: true, retainRoute: true, signature: result.code };
73
+ return { outcome: 'failed', signature: result.code };
65
74
  return { outcome: 'unknown', signature: result.code };
66
75
  },
67
76
  };
@@ -126,7 +135,7 @@ export async function observeOpenCodeContext(sessionId, messages, env, signal) {
126
135
  }
127
136
  /** The native receiver owns no claims outside the artifact; HOME is used only for discovery. */
128
137
  export async function receiveOpenCodePending(target, env, signal) {
129
- const observer = await observeSessionPending(bodyFor(target.sessionId), env);
138
+ const observer = await observeSessionPending(ownerFor(target.sessionId), env);
130
139
  const transport = createOpenCodeWakeTransport(target, signal);
131
140
  let waiting;
132
141
  try {
@@ -181,7 +190,8 @@ export async function receiveOpenCodePending(target, env, signal) {
181
190
  }
182
191
  /** Publish the native address into current artifact seats; the legacy route projection is not read. */
183
192
  export async function publishOpenCodeAddress(sessionId, env, signal) {
184
- const seats = await discoverOpenCodeSeats(sessionId, env, signal);
193
+ await restoreSessionSeats(ownerFor(sessionId), env, { kind: routeKind, address: { sessionId } }, signal);
194
+ const seats = (await discoverOpenCodeSeats(sessionId, env, signal)).filter(({ seat }) => seat.attachment === 'attached');
185
195
  for (const { location, seat } of seats) {
186
196
  if (signal.aborted)
187
197
  return false;
@@ -199,6 +209,8 @@ export async function cancelOpenCodeDeliveries(sessionId, env, signal) {
199
209
  for (const { location, seat } of await discoverOpenCodeSeats(sessionId, env, signal)) {
200
210
  if (signal.aborted)
201
211
  return;
212
+ if (seat.attachment !== 'attached')
213
+ continue;
202
214
  const artifact = createArtifactPort(location, undefined, signal);
203
215
  try {
204
216
  const { value: through } = await artifact.read((facts) => facts.header().lastActIndex, signal);
@@ -215,9 +227,9 @@ export async function endOpenCodeSession(sessionId, env, signal) {
215
227
  if (signal.aborted)
216
228
  return;
217
229
  const { Square } = await import('../../square-api.js');
218
- const square = await Square.at({ path: location, env, body: seat.body, participant: seat.participant });
230
+ const square = await Square.at({ path: location, env });
219
231
  try {
220
- await square.endOwnedSession(seat.participant, sessionId, seat.generation, seat.body);
232
+ await square.endHeld(seat.participant, { owner: seat.owner, generation: seat.generation });
221
233
  }
222
234
  finally {
223
235
  await square.close();
package/dist/ports.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Act, ActivityId, Body, HardCap, Seat, StoredAct, WakeAddress, WakeRoute, WatchLease } from './model.js';
1
+ import type { Act, ActivityId, Owner, HardCap, Seat, StoredAct, WakeAddress, WakeRoute, WatchLease } from './model.js';
2
2
  import type { MetadataMatch, DeliveryDeferralFact } from './delivery-deferral.js';
3
3
  export type ActivityIndex = number;
4
4
  export interface SquareHeader {
@@ -85,22 +85,22 @@ export interface ReceiptAssignment {
85
85
  readonly owner: string;
86
86
  readonly index: ActivityIndex;
87
87
  }
88
- export type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
89
- readonly [key: string]: JsonValue;
90
- };
91
- export type SeatRef = Omit<Seat, 'wakeAddress'>;
88
+ export type SeatRef = Pick<Seat, 'participant' | 'owner' | 'generation'>;
92
89
  export type ArtifactWriteAuthority = {
93
90
  readonly kind: 'participant';
94
91
  readonly seat: SeatRef;
92
+ } | {
93
+ readonly kind: 'host-cleanup';
94
+ readonly seat: SeatRef;
95
95
  } | {
96
96
  readonly kind: 'join';
97
97
  readonly participant: string;
98
- readonly body: Body;
98
+ readonly owner: Owner;
99
99
  readonly wakeAddress?: WakeAddress;
100
100
  } | {
101
101
  readonly kind: 'takeover';
102
102
  readonly participant: string;
103
- readonly body: Body;
103
+ readonly owner: Owner;
104
104
  readonly expectedGeneration: ActivityId;
105
105
  readonly wakeAddress?: WakeAddress;
106
106
  };
@@ -168,9 +168,7 @@ export interface DeliveryCompletion {
168
168
  /** Domain facts exist only during a synchronous artifact callback. Values are detached. */
169
169
  export interface SquareRead {
170
170
  seat(requestedName: string): Seat | undefined;
171
- seatsForBody(body: Body): readonly Seat[];
172
- /** Latest completed coordinates only; never participant write authority. */
173
- completedSeatsForBody(body: Body): readonly Seat[];
171
+ seatsForOwner(owner: Owner): readonly Seat[];
174
172
  deliveryEvidence(participant: string, generation: ActivityId, activity?: ActivityId): readonly DeliveryEvidence[];
175
173
  deliveryByToken(claimToken: string): DeliveryEvidence | undefined;
176
174
  cancelledThrough(participant: string, generation: ActivityId): number | undefined;
@@ -219,6 +217,10 @@ export interface SquareWrite extends SquareRead {
219
217
  readonly at: number;
220
218
  }): StoredAct;
221
219
  updateWakeAddress(seat: SeatRef, wakeAddress?: WakeAddress): boolean;
220
+ /** Join authority only; preserves membership and its establishing generation. */
221
+ reattachSeat(seat: SeatRef, wakeAddress?: WakeAddress): boolean;
222
+ /** Host-cleanup authority only; stale/already-detached coordinates are a no-op. */
223
+ detachSeat(seat: SeatRef): boolean;
222
224
  cancelThrough(seat: SeatRef, through: ActivityIndex): boolean;
223
225
  claimDelivery(input: DeliveryClaimInput): DeliveryClaimResult;
224
226
  prepareDelivery(input: DeliveryPreparation): boolean;
@@ -245,7 +247,7 @@ export interface SquareCommitResult<R> extends SquareReadResult<R> {
245
247
  /** Atomic artifact access; no framing, SQL or lazy transaction capability escapes. */
246
248
  export interface SquareArtifactPort {
247
249
  read<R>(select: (facts: SquareRead) => R, signal?: AbortSignal): Promise<SquareReadResult<R>>;
248
- /** Participant/domain writes must carry an explicit authority; no body-less mutation path exists. */
250
+ /** Participant/domain writes must carry an explicit owner; no owner-less mutation path exists. */
249
251
  transactAs<R>(authority: ArtifactWriteAuthority, change: (facts: SquareWrite) => R, signal?: AbortSignal): Promise<SquareCommitResult<R>>;
250
252
  /** Bounded cleanup/evidence settlement on this same connection after custody was acquired.
251
253
  * Independent of request/lifetime cancellation; explicit close and authority fences still apply. */
@@ -295,28 +297,14 @@ export interface WakeRequest {
295
297
  export interface PresentationSinkPort {
296
298
  present(activity: StoredAct): void | Promise<void>;
297
299
  }
298
- /** Read-only presentation evidence projection for diagnostics and migration callers. */
299
- export interface PresentationEvidenceProjection {
300
- readonly location: string;
301
- readonly participant: string;
302
- readonly sessionId: string;
303
- readonly activity: string;
304
- readonly outcome: string;
305
- readonly at?: number;
306
- }
307
300
  export type WakeOutcome = {
308
301
  readonly outcome: 'accepted';
309
302
  readonly signature?: string;
310
- readonly attemptN?: number;
311
303
  } | {
312
304
  readonly outcome: 'failed';
313
305
  readonly message?: string;
314
306
  readonly signature?: string;
315
307
  readonly diagnostic?: unknown;
316
- readonly attemptN?: number;
317
- readonly unavailable?: boolean;
318
- readonly retainRoute?: boolean;
319
- readonly routeStale?: boolean;
320
308
  } | {
321
309
  readonly outcome: 'not-capable';
322
310
  readonly diagnostic?: string;
@@ -325,15 +313,7 @@ export type WakeOutcome = {
325
313
  readonly signature?: string;
326
314
  readonly message?: string;
327
315
  readonly diagnostic?: unknown;
328
- readonly attemptN?: number;
329
316
  };
330
- export interface DeliveryResult {
331
- readonly attempted: number;
332
- readonly accepted: number;
333
- readonly failed: number;
334
- readonly unknown: number;
335
- readonly notCapable: number;
336
- }
337
317
  export interface DeliverPendingInput {
338
318
  readonly artifact: SquareArtifactPort;
339
319
  readonly transport: WakeTransportPort;