@astrosheep/square 0.3.92 → 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 (119) 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/harness-links.d.ts +1 -10
  36. package/dist/harness-links.js +144 -109
  37. package/dist/harness.js +4 -13
  38. package/dist/help.js +12 -10
  39. package/dist/inbox.d.ts +9 -22
  40. package/dist/inbox.js +23 -27
  41. package/dist/index.d.ts +1 -3
  42. package/dist/index.js +1 -1
  43. package/dist/mcp.d.ts +3 -0
  44. package/dist/mcp.js +23 -24
  45. package/dist/model.d.ts +8 -15
  46. package/dist/model.js +4 -7
  47. package/dist/notifications.d.ts +1 -1
  48. package/dist/notifications.js +6 -5
  49. package/dist/operation-presentation.d.ts +5 -4
  50. package/dist/operation-presentation.js +50 -45
  51. package/dist/owner.d.ts +11 -0
  52. package/dist/owner.js +32 -0
  53. package/dist/paseo-delivery.d.ts +4 -1
  54. package/dist/paseo-delivery.js +7 -12
  55. package/dist/paseo-state.d.ts +2 -2
  56. package/dist/paseo-state.js +6 -4
  57. package/dist/plugins/opencode/delivery.js +27 -15
  58. package/dist/ports.d.ts +13 -33
  59. package/dist/presentation-operations.js +4 -4
  60. package/dist/presentation.d.ts +5 -8
  61. package/dist/presentation.js +33 -43
  62. package/dist/runtime.d.ts +1 -7
  63. package/dist/runtime.js +0 -14
  64. package/dist/seats.d.ts +2 -3
  65. package/dist/seats.js +7 -15
  66. package/dist/session-lifecycle.d.ts +6 -2
  67. package/dist/session-lifecycle.js +39 -11
  68. package/dist/sqlite-square.d.ts +36 -22
  69. package/dist/sqlite-square.js +138 -161
  70. package/dist/square-api.d.ts +15 -10
  71. package/dist/square-api.js +26 -22
  72. package/dist/square-core.d.ts +0 -5
  73. package/dist/square-core.js +0 -9
  74. package/dist/square-domain.d.ts +4 -2
  75. package/dist/square-domain.js +56 -21
  76. package/dist/square-facade.d.ts +45 -87
  77. package/dist/square-index.d.ts +5 -13
  78. package/dist/square-index.js +13 -38
  79. package/dist/square-location.d.ts +1 -14
  80. package/dist/square-location.js +4 -69
  81. package/dist/square-migration-host-ledger.d.ts +2 -2
  82. package/dist/square-migration-host-ledger.js +6 -5
  83. package/dist/square-migration.d.ts +2 -1
  84. package/dist/square-migration.js +52 -30
  85. package/dist/square-sqlite.d.ts +6 -2
  86. package/dist/square-sqlite.js +54 -28
  87. package/dist/square-tools.d.ts +7 -10
  88. package/dist/square-tools.js +22 -54
  89. package/dist/stream.js +4 -5
  90. package/dist/views.d.ts +21 -10
  91. package/dist/views.js +38 -38
  92. package/dist/wakes.d.ts +1 -2
  93. package/dist/wakes.js +1 -1
  94. package/guides/application-facade-delivery-evidence.md +1 -1
  95. package/guides/claude-native.md +18 -9
  96. package/guides/codex-delivery.md +17 -6
  97. package/guides/mcp-connection-lifetime.md +1 -1
  98. package/guides/mcp.md +3 -3
  99. package/guides/migration.md +142 -151
  100. package/guides/operation-cancellation.md +1 -1
  101. package/guides/participant-ownership.md +78 -47
  102. package/guides/pi-tools.md +33 -4
  103. package/guides/primitive-history-consolidation.md +1 -1
  104. package/guides/row-storage-design.md +8 -8
  105. package/guides/square-api.md +244 -88
  106. package/package.json +2 -1
  107. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  108. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  109. package/plugins/opencode/delivery.ts +27 -17
  110. package/plugins/paseo/client/palette.ts +18 -18
  111. package/plugins/paseo/client/square-message.tsx +17 -23
  112. package/plugins/pi/square-pi.js +17 -17
  113. package/dist/square-backend.d.ts +0 -13
  114. package/dist/square-backend.js +0 -1
  115. package/dist/square-stdio.d.ts +0 -22
  116. package/dist/square-stdio.js +0 -235
  117. package/dist/stdio-server.d.ts +0 -3
  118. package/dist/stdio-server.js +0 -212
  119. package/guides/backends.md +0 -193
package/dist/mcp.js CHANGED
@@ -6,8 +6,10 @@ import { codexWakeAddress } from './codex-address.js';
6
6
  import { readClaudeMcpCall } from './claude-mcp-context.js';
7
7
  import { SQUARE_IDENTITY } from './identity.js';
8
8
  import { Square, createSquareResourceOwner } from './square-api.js';
9
+ import { harnessOwner } from './square-api.js';
9
10
  export function createSquareMcpServer(options) {
10
- const baseEnv = { ...(options.env ?? process.env) };
11
+ const { owner: configuredOwner, wakeAddress, ...resourceOptions } = options;
12
+ const baseEnv = { ...(resourceOptions.env ?? process.env) };
11
13
  const resources = createSquareResourceOwner();
12
14
  // Calls to the configured square borrow one server-owned connection.
13
15
  const inFlight = new Set();
@@ -19,21 +21,20 @@ export function createSquareMcpServer(options) {
19
21
  const call = (async () => {
20
22
  let square;
21
23
  try {
22
- const { as: participant, location } = args;
24
+ const { location } = args;
23
25
  const threadId = extra._meta?.threadId;
24
26
  const opencodeSessionId = extra._meta?.['ai.opencode/sessionID'];
25
27
  // Claude's plugin endpoint requires the mod's call-local coordinate, never startup identity.
26
28
  const claude = baseEnv.SQUARE_CLAUDE_MCP_CONTEXT === '1'
27
29
  ? await readClaudeMcpCall(extra._meta?.['claudecode/toolUseId'], baseEnv)
28
30
  : undefined;
29
- const body = claude !== undefined
30
- ? { channel: 'claude-code', session: claude.sessionId,
31
- ...(claude.agentId === undefined ? {} : { agent: claude.agentId }) }
31
+ const owner = claude !== undefined
32
+ ? harnessOwner({ channel: 'claude-code', session: claude.sessionId, ...(claude.agentId === undefined ? {} : { agent: claude.agentId }) })
32
33
  : typeof threadId === 'string' && threadId.trim()
33
- ? { channel: 'codex', session: threadId }
34
+ ? harnessOwner({ channel: 'codex', session: threadId })
34
35
  : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
35
- ? { channel: 'opencode', session: opencodeSessionId }
36
- : options.body;
36
+ ? harnessOwner({ channel: 'opencode', session: opencodeSessionId })
37
+ : configuredOwner;
37
38
  const env = claude !== undefined
38
39
  ? { ...operationEnv('claude', claude.sessionId, baseEnv),
39
40
  // Preserve Claude's session + child flag semantics; an agent loop ID is not a session ID.
@@ -44,37 +45,35 @@ export function createSquareMcpServer(options) {
44
45
  ? operationEnv('opencode', opencodeSessionId, baseEnv)
45
46
  : baseEnv;
46
47
  square = await Square.at({
47
- ...options, env, body, cwd: claude?.cwd ?? options.cwd,
48
- // A call-local Codex thread uses its native queue even when managed by Paseo.
49
- wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
50
- ? codexWakeAddress(threadId, env) : options.wakeAddress,
48
+ ...resourceOptions, env, cwd: claude?.cwd ?? resourceOptions.cwd,
51
49
  path: location ?? options.path,
52
- participant: participant ?? options.participant,
53
50
  // The server owner is bound to its configured square; overrides close after each call.
54
51
  resources: location === undefined && claude === undefined ? resources : undefined,
55
52
  }, { signal: extra.signal });
56
- let receipt;
57
- // The transport shows people the square, so it asks for the rendering facts
58
- // the CLI already gathers and keeps that request out of the tool schema.
53
+ const callWake = wakeAddress !== undefined && (owner === configuredOwner
54
+ || wakeAddress.kind === 'paseo' && wakeAddress.address.agentId === baseEnv.PASEO_AGENT_ID
55
+ || wakeAddress.kind === 'claude-native' && wakeAddress.address.sessionId === claude?.sessionId
56
+ || wakeAddress.kind === 'opencode-server' && wakeAddress.address.sessionId === opencodeSessionId)
57
+ ? wakeAddress : undefined;
58
+ let text;
59
59
  try {
60
- receipt = await tool.run(square, args, { signal: extra.signal, observe: true });
60
+ text = await tool.run(square, args, { signal: extra.signal }, {
61
+ owner, wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
62
+ ? codexWakeAddress(threadId, env) : callWake,
63
+ });
61
64
  }
62
65
  finally {
63
66
  await square.close();
64
67
  }
65
- // A missing observation renders as nothing, never as raw JSON for a person to read.
66
- const text = tool.render(receipt, args) ?? '';
67
- // Observation facts are a rendering aid, not part of the receipt machines receive.
68
- const { observation: _observation, ...structuredContent } = receipt;
69
- return { content: [{ type: 'text', text }], structuredContent };
68
+ return { content: [{ type: 'text', text }] };
70
69
  }
71
70
  catch (error) {
72
71
  const failure = squareToolFailure(error, extra.signal);
73
72
  const participant = args.as;
74
73
  return { isError: true, content: [{ type: 'text', text: squareToolFailureText(failure, {
75
74
  location: square?.location ?? options.path,
76
- participant: typeof participant === 'string' ? participant : options.participant,
77
- }) }], structuredContent: { error: failure } };
75
+ participant: typeof participant === 'string' ? participant : undefined,
76
+ }) }] };
78
77
  }
79
78
  })();
80
79
  inFlight.add(call);
package/dist/model.d.ts CHANGED
@@ -12,11 +12,8 @@ export declare const ACTIVITY_METADATA_MAX_DEPTH = 256;
12
12
  /** Validate the bounded, lossless JSON object carried by a say. */
13
13
  export declare function validateActivityMetadata(value: unknown): ActivityMetadata | undefined;
14
14
  export declare function activityMetadataHasContent(metadata: ActivityMetadata | undefined): boolean;
15
- export interface Body {
16
- readonly channel: string;
17
- readonly session: string;
18
- readonly agent?: string;
19
- }
15
+ /** Opaque custody identity. Harness coordinates are adapter-owned. */
16
+ export type Owner = string;
20
17
  export interface WakeAddress {
21
18
  readonly kind: WakeRouteKind;
22
19
  readonly address: Readonly<Record<string, string>>;
@@ -24,24 +21,22 @@ export interface WakeAddress {
24
21
  /** Artifact-owned custody binding. Generation is the join activity that established it. */
25
22
  export interface Seat {
26
23
  readonly participant: string;
27
- readonly body: Body;
24
+ readonly owner: Owner;
28
25
  readonly generation: ActivityId;
26
+ readonly attachment: 'attached' | 'detached';
29
27
  readonly wakeAddress?: WakeAddress;
30
28
  }
31
29
  export interface SeatWatchLease {
32
30
  readonly generation: ActivityId;
33
31
  readonly lease: WatchLease;
34
32
  }
35
- export declare function sameBody(left: Body, right: Body): boolean;
36
- export declare function validateBody(body: Body): void;
33
+ export declare function validateOwner(owner: Owner): Owner;
37
34
  export declare function isWakeRouteKind(value: unknown): value is WakeRouteKind;
38
35
  export interface WakeRoute {
39
36
  /** Ownership generation, when the receiver publishes a fenced route. */
40
37
  epoch?: number;
41
38
  location: string;
42
39
  participant: string;
43
- sessionId: string;
44
- channel: string;
45
40
  kind: WakeRouteKind;
46
41
  address: Record<string, string>;
47
42
  updatedAt: number;
@@ -49,10 +44,12 @@ export interface WakeRoute {
49
44
  export type SquareErrorCode = 'invalid_name' | 'invalid_args' | 'unknown_participant' | 'not_joined' | 'already_joined' | 'already_done' | 'held' | 'capped' | 'throttled' | 'bell_quota' | 'behind' | 'io' | 'unavailable';
50
45
  export type InternalSquareErrorCode = SquareErrorCode | 'not_found' | 'cap_reached' | 'conflict' | 'pending_peer';
51
46
  export interface SquareErrorFacts {
47
+ /** Copyable recovery for a missing, detached or foreign seat. */
48
+ join?: string;
52
49
  /** Express admission rejected before any activity was committed. */
53
50
  activityUnsent?: true;
54
51
  /** Refusal reason, including absent participants, conflicting reach, and inaccessible activities. */
55
- reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'migration_required' | 'invalid_reply' | 'invalid_done_body';
52
+ reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'migration_required' | 'invalid_reply' | 'invalid_done_body' | 'needs_seat';
56
53
  /** The activity id a `catch --id` refusal named. */
57
54
  activityId?: string;
58
55
  /** The host ledger requiring offline migration, rather than the selected artifact. */
@@ -122,13 +119,9 @@ export interface PendingMembership extends LocatedSeat {
122
119
  readonly deliveryDeferralExpiry?: number;
123
120
  }
124
121
  export interface WatchOptions {
125
- id?: ActivityId;
126
122
  participants?: string[];
127
123
  mention?: string;
128
- limit?: number;
129
- idleMs?: number;
130
124
  replace?: boolean;
131
- now?: boolean;
132
125
  }
133
126
  export type PublicAct = Extract<StoredAct, {
134
127
  kind: 'say' | 'done';
package/dist/model.js CHANGED
@@ -76,14 +76,11 @@ export function validateActivityMetadata(value) {
76
76
  export function activityMetadataHasContent(metadata) {
77
77
  return metadata !== undefined && Object.keys(metadata).length > 0;
78
78
  }
79
- export function sameBody(left, right) {
80
- return left.channel === right.channel && left.session === right.session && left.agent === right.agent;
81
- }
82
- export function validateBody(body) {
83
- if (!body || typeof body.channel !== 'string' || !body.channel || typeof body.session !== 'string' || !body.session
84
- || (body.agent !== undefined && (typeof body.agent !== 'string' || !body.agent))) {
85
- throw new SquareError('invalid_args', 'A participant write requires an explicit channel and session body.');
79
+ export function validateOwner(owner) {
80
+ if (typeof owner !== 'string' || !owner || Buffer.byteLength(owner, 'utf8') > 512 || /[\u0000-\u001f\u007f-\u009f]/u.test(owner)) {
81
+ throw new SquareError('invalid_args', 'A participant write requires a non-empty owner without control characters (at most 512 UTF-8 bytes).');
86
82
  }
83
+ return owner;
87
84
  }
88
85
  export function isWakeRouteKind(value) {
89
86
  return typeof value === 'string' && WAKE_ROUTE_KINDS.includes(value);
@@ -13,7 +13,7 @@ interface ProcessNotificationOptions {
13
13
  export declare function createDefaultWakeTransport(env?: NodeJS.ProcessEnv, suppliedTransports?: WakeTransports): Promise<WakeTransportPort>;
14
14
  /** Route selection only: all implementations receive the same request and final gate. */
15
15
  export declare function createWakeTransport(transports: WakeTransports): WakeTransportPort;
16
- export declare function processActNotificationsOnce(squarePath: string, actIndex: number, opts?: ProcessNotificationOptions): Promise<import("./ports.js").DeliveryResult>;
16
+ export declare function processActNotificationsOnce(squarePath: string, actIndex: number, opts?: ProcessNotificationOptions): Promise<void>;
17
17
  /** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares within one boundary budget. */
18
18
  export declare function sweepPrivilegedPending(cwd: string, env?: NodeJS.ProcessEnv, suppliedTransports?: WakeTransports, deadline?: number, signal?: AbortSignal): Promise<void>;
19
19
  export interface SweepPendingNotificationsOptions {
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import { canonicalPath } from './canonical-path.js';
2
3
  import fs from 'node:fs';
3
4
  import { indexedSquareEntries } from './square-index.js';
4
5
  import { InternalSquareError, SquareError } from './model.js';
@@ -55,16 +56,15 @@ export function createWakeTransport(transports) {
55
56
  export async function processActNotificationsOnce(squarePath, actIndex, opts = {}) {
56
57
  const env = opts.env ?? process.env;
57
58
  const now = opts.now ?? Date.now;
59
+ squarePath = await canonicalPath(squarePath);
58
60
  const artifact = createArtifactPort(squarePath);
59
61
  try {
60
62
  await artifact.read(facts => facts.header());
61
63
  const transport = await createDefaultWakeTransport(env, opts.transports);
62
64
  try {
63
- return await deliverPending({ artifact, transport, location: squarePath, activity: actIndex, timeoutMs: Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000), now: now() });
64
- }
65
- catch {
66
- return { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
65
+ await deliverPending({ artifact, transport, location: squarePath, activity: actIndex, timeoutMs: Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000), now: now() });
67
66
  }
67
+ catch { /* Delivery failure does not undo the activity. */ }
68
68
  }
69
69
  catch (error) {
70
70
  if (error instanceof InternalSquareError && error.code === 'not_found')
@@ -92,10 +92,11 @@ export async function sweepPrivilegedPending(cwd, env = process.env, suppliedTra
92
92
  if (remainingMs() === 0 || signal?.aborted)
93
93
  return;
94
94
  const transport = await createDefaultWakeTransport(env, suppliedTransports);
95
- for (const squarePath of paths) {
95
+ for (const candidate of paths) {
96
96
  if (remainingMs() === 0 || signal?.aborted)
97
97
  break;
98
98
  try {
99
+ const squarePath = await canonicalPath(candidate);
99
100
  const artifact = createArtifactPort(squarePath, undefined, signal);
100
101
  try {
101
102
  await artifact.read(facts => facts.header(), signal);
@@ -7,10 +7,10 @@ export type JoinPresentation = Omit<JoinResult, 'participant'> & {
7
7
  readonly name: string;
8
8
  } | string;
9
9
  };
10
- export declare function renderJoinResult(data: JoinPresentation): string | undefined;
11
- export declare function renderExpression(squarePath: string, data: ExpressResult): string | undefined;
10
+ export declare function renderJoinResult(data: JoinPresentation, owner?: string): string | undefined;
11
+ export declare function renderExpression(squarePath: string, data: ExpressResult, owner?: string): string | undefined;
12
12
  /** Render settled catch facts; no selection, terminal decisions, receipts or wait machinery. */
13
- export declare function renderCatchOutcome(squarePath: string, data: CatchResult, options?: CatchOptions): {
13
+ export declare function renderCatchOutcome(squarePath: string, data: CatchResult, options?: CatchOptions, owner?: string): {
14
14
  stdout: string;
15
15
  exitCode: number;
16
16
  };
@@ -31,7 +31,7 @@ export interface FailureReport {
31
31
  readonly code: string;
32
32
  readonly message: string;
33
33
  readonly facts?: {
34
- readonly reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'migration_required' | 'invalid_reply' | 'invalid_done_body';
34
+ readonly reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'migration_required' | 'invalid_reply' | 'invalid_done_body' | 'needs_seat';
35
35
  readonly activityId?: string;
36
36
  readonly ledgerRoot?: string;
37
37
  readonly activityExists?: boolean;
@@ -42,4 +42,5 @@ export interface FailureReport {
42
42
  export declare function renderFailureLines(failure: FailureReport, options?: {
43
43
  squarePath?: string;
44
44
  name?: string;
45
+ owner?: string;
45
46
  }): string[];
@@ -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
  }