@astrosheep/square 0.3.63 → 0.3.65

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 (171) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +3 -2
  2. package/claude-plugin/hooks/hooks.json +1 -22
  3. package/claude-plugin/hooks/register.js +119 -0
  4. package/claude-plugin/types.d.ts +7 -0
  5. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  6. package/dist/activity-feed.d.ts +1 -10
  7. package/dist/activity-feed.js +1 -27
  8. package/dist/activity.js +57 -25
  9. package/dist/artifact.d.ts +1 -1
  10. package/dist/artifact.js +24 -25
  11. package/dist/attention-presentation.js +1 -0
  12. package/dist/automatic-session.d.ts +3 -1
  13. package/dist/automatic-session.js +21 -34
  14. package/dist/boundary-presentation.js +4 -13
  15. package/dist/canonical-path.d.ts +11 -0
  16. package/dist/canonical-path.js +47 -0
  17. package/dist/catch-decisions.js +14 -10
  18. package/dist/catch-selection.d.ts +6 -0
  19. package/dist/catch-selection.js +14 -0
  20. package/dist/claude-delivery.d.ts +11 -0
  21. package/dist/claude-delivery.js +145 -0
  22. package/dist/claude-mod.d.ts +8 -0
  23. package/dist/claude-mod.js +164 -0
  24. package/dist/cli/context.d.ts +0 -1
  25. package/dist/cli/context.js +9 -21
  26. package/dist/cli/harness-command.d.ts +0 -1
  27. package/dist/cli/harness-command.js +0 -6
  28. package/dist/cli/observation-commands.d.ts +1 -1
  29. package/dist/cli/observation-commands.js +31 -58
  30. package/dist/cli/program.js +20 -18
  31. package/dist/cli/registry.js +2 -2
  32. package/dist/cli/square-commands.js +10 -6
  33. package/dist/codex-boundary-state.d.ts +2 -2
  34. package/dist/codex-boundary-state.js +4 -4
  35. package/dist/codex-hook.js +12 -15
  36. package/dist/codex-queue.d.ts +2 -2
  37. package/dist/codex-queue.js +7 -5
  38. package/dist/decisions.d.ts +2 -6
  39. package/dist/decisions.js +16 -17
  40. package/dist/delivery-health.d.ts +1 -2
  41. package/dist/delivery-health.js +4 -5
  42. package/dist/delivery-operations.d.ts +2 -5
  43. package/dist/delivery-operations.js +77 -214
  44. package/dist/delivery.d.ts +25 -47
  45. package/dist/delivery.js +14 -46
  46. package/dist/diagnostic-redaction.js +7 -9
  47. package/dist/file-changes.d.ts +0 -1
  48. package/dist/file-changes.js +2 -19
  49. package/dist/harness-claude.js +8 -6
  50. package/dist/harness-codex.d.ts +0 -2
  51. package/dist/harness-codex.js +0 -2
  52. package/dist/harness-links.js +13 -17
  53. package/dist/harness.js +3 -7
  54. package/dist/help.js +4 -3
  55. package/dist/host-ledger-file-adapter.d.ts +17 -8
  56. package/dist/host-ledger-file-adapter.js +164 -80
  57. package/dist/host-ledger.d.ts +55 -50
  58. package/dist/inbox.d.ts +0 -3
  59. package/dist/inbox.js +88 -50
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js +1 -1
  62. package/dist/list.js +1 -4
  63. package/dist/mcp.js +24 -41
  64. package/dist/mention-parse.js +3 -9
  65. package/dist/model.d.ts +11 -2
  66. package/dist/model.js +1 -1
  67. package/dist/notifications.d.ts +3 -16
  68. package/dist/notifications.js +41 -122
  69. package/dist/opencode-delivery.d.ts +14 -0
  70. package/dist/opencode-delivery.js +171 -0
  71. package/dist/opencode.d.ts +39 -18
  72. package/dist/opencode.js +145 -40
  73. package/dist/packages/agent-delivery/src/claude-auth.d.ts +13 -0
  74. package/dist/packages/agent-delivery/src/claude-auth.js +75 -0
  75. package/dist/packages/agent-delivery/src/claude-native.d.ts +28 -0
  76. package/dist/packages/agent-delivery/src/claude-native.js +68 -0
  77. package/dist/packages/agent-delivery/src/claude.d.ts +14 -0
  78. package/dist/packages/agent-delivery/src/claude.js +66 -0
  79. package/dist/packages/agent-delivery/src/deadline.d.ts +11 -0
  80. package/dist/packages/agent-delivery/src/deadline.js +41 -0
  81. package/dist/packages/agent-delivery/src/errors.d.ts +16 -0
  82. package/dist/packages/agent-delivery/src/errors.js +41 -0
  83. package/dist/packages/agent-delivery/src/guards.d.ts +6 -0
  84. package/dist/packages/agent-delivery/src/guards.js +6 -0
  85. package/dist/packages/agent-delivery/src/index.d.ts +83 -0
  86. package/dist/packages/agent-delivery/src/index.js +110 -0
  87. package/dist/packages/agent-delivery/src/local-endpoint.d.ts +26 -0
  88. package/dist/packages/agent-delivery/src/local-endpoint.js +114 -0
  89. package/dist/packages/agent-delivery/src/opencode-native.d.ts +33 -0
  90. package/dist/packages/agent-delivery/src/opencode-native.js +75 -0
  91. package/dist/packages/agent-delivery/src/opencode.d.ts +14 -0
  92. package/dist/packages/agent-delivery/src/opencode.js +127 -0
  93. package/dist/packages/agent-delivery/src/paseo-native.d.ts +58 -0
  94. package/dist/packages/agent-delivery/src/paseo-native.js +122 -0
  95. package/dist/packages/agent-delivery/src/paseo.d.ts +19 -0
  96. package/dist/packages/agent-delivery/src/paseo.js +106 -0
  97. package/dist/packages/agent-delivery/src/pi-client.d.ts +10 -0
  98. package/dist/packages/agent-delivery/src/pi-client.js +106 -0
  99. package/dist/packages/agent-delivery/src/pi-protocol.d.ts +15 -0
  100. package/dist/packages/agent-delivery/src/pi-protocol.js +50 -0
  101. package/dist/packages/agent-delivery/src/pi-send.d.ts +23 -0
  102. package/dist/packages/agent-delivery/src/pi-send.js +4 -0
  103. package/dist/packages/agent-delivery/src/pi.d.ts +25 -0
  104. package/dist/packages/agent-delivery/src/pi.js +209 -0
  105. package/dist/packages/agent-delivery/src/types.d.ts +120 -0
  106. package/dist/participant-host.js +12 -13
  107. package/dist/participant-identity.d.ts +21 -0
  108. package/dist/participant-identity.js +32 -11
  109. package/dist/paseo-delivery.d.ts +2 -2
  110. package/dist/paseo-delivery.js +35 -27
  111. package/dist/paseo-timeline.js +25 -21
  112. package/dist/paseo.d.ts +2 -2
  113. package/dist/paseo.js +1 -2
  114. package/dist/ports.d.ts +7 -20
  115. package/dist/presentation-operations.js +16 -4
  116. package/dist/presentation.d.ts +19 -10
  117. package/dist/presentation.js +99 -177
  118. package/dist/registry.d.ts +15 -66
  119. package/dist/registry.js +45 -123
  120. package/dist/routes.d.ts +1 -28
  121. package/dist/routes.js +22 -104
  122. package/dist/runtime.d.ts +3 -9
  123. package/dist/runtime.js +14 -67
  124. package/dist/search.d.ts +0 -2
  125. package/dist/search.js +3 -10
  126. package/dist/square-actions.js +48 -72
  127. package/dist/square-application.js +32 -30
  128. package/dist/square-core.d.ts +3 -0
  129. package/dist/square-core.js +22 -5
  130. package/dist/square-facade.d.ts +2 -0
  131. package/dist/square-file-adapter.d.ts +2 -1
  132. package/dist/square-file-adapter.js +6 -9
  133. package/dist/square-projections.d.ts +40 -45
  134. package/dist/square-projections.js +60 -74
  135. package/dist/square-state.js +1 -4
  136. package/dist/square-storage.d.ts +5 -8
  137. package/dist/square-storage.js +13 -36
  138. package/dist/square-wiring.d.ts +2 -7
  139. package/dist/square-wiring.js +6 -24
  140. package/dist/stream.js +4 -6
  141. package/dist/time.js +2 -7
  142. package/dist/views.d.ts +6 -18
  143. package/dist/views.js +14 -21
  144. package/dist/wake-attempts.d.ts +2 -56
  145. package/dist/wake-attempts.js +15 -110
  146. package/dist/wake-eligibility.d.ts +43 -0
  147. package/dist/wake-eligibility.js +106 -0
  148. package/dist/wake-evidence.d.ts +32 -9
  149. package/dist/wake-evidence.js +29 -24
  150. package/dist/wake-sink.d.ts +13 -9
  151. package/dist/wake-sink.js +35 -54
  152. package/dist/watch.js +13 -21
  153. package/extensions/square-pi.js +27 -13
  154. package/guides/change-waits.md +1 -1
  155. package/guides/claude-native.md +101 -0
  156. package/guides/host-ledger.md +3 -1
  157. package/guides/participant.md +14 -7
  158. package/guides/pi-native-delivery.md +49 -0
  159. package/package.json +4 -3
  160. package/dist/claude-hook.d.ts +0 -10
  161. package/dist/claude-hook.js +0 -60
  162. package/dist/paseo-connection.d.ts +0 -15
  163. package/dist/paseo-connection.js +0 -141
  164. package/dist/presence.d.ts +0 -2
  165. package/dist/presence.js +0 -24
  166. package/dist/presented.d.ts +0 -2
  167. package/dist/presented.js +0 -10
  168. package/dist/state-cell.d.ts +0 -14
  169. package/dist/wake-port.d.ts +0 -23
  170. package/dist/wake-port.js +0 -27
  171. /package/dist/{state-cell.js → packages/agent-delivery/src/types.js} +0 -0
@@ -0,0 +1,43 @@
1
+ import { type SquareState, type WakeRoute } from './model.js';
2
+ import { deriveDeliveryModel } from './delivery.js';
3
+ import type { EvidenceRecord, PresenceRecord } from './host-ledger.js';
4
+ import type { PresentationEvidenceProjection, WakeRequest } from './ports.js';
5
+ import { type WakeAttempt } from './wake-evidence.js';
6
+ export interface WakeSnapshot {
7
+ readonly state: SquareState;
8
+ /** Canonicalized by the caller; every identity in this snapshot is square-local. */
9
+ readonly location: string;
10
+ readonly owners: readonly PresenceRecord[];
11
+ readonly wakeRecords: readonly EvidenceRecord[];
12
+ readonly presentations: readonly PresentationEvidenceProjection[];
13
+ readonly now: number;
14
+ readonly delivery?: ReturnType<typeof deriveDeliveryModel>;
15
+ }
16
+ export interface OwnedWakeRoute {
17
+ readonly route: WakeRoute;
18
+ readonly owner: PresenceRecord;
19
+ }
20
+ export interface WakeCurrentness {
21
+ readonly current: boolean;
22
+ readonly activityPending: boolean;
23
+ readonly sessionBound: boolean;
24
+ readonly routePublished: boolean;
25
+ readonly selectedOwner: boolean;
26
+ readonly cancelled: boolean;
27
+ readonly presented: boolean;
28
+ }
29
+ export interface WakeEvidence {
30
+ readonly delivered: boolean;
31
+ /** All selected, uncancelled routes have session-local presentation evidence. */
32
+ readonly presented: boolean;
33
+ readonly attempts: readonly WakeAttempt[];
34
+ readonly terminal?: WakeAttempt;
35
+ readonly attemptableRoutes: readonly WakeRoute[];
36
+ }
37
+ export interface WakeEligibility {
38
+ evidence(recipient: string, actIndex: number): WakeEvidence;
39
+ currentness(request: WakeRequest): WakeCurrentness;
40
+ }
41
+ /** One strict ownership join, latest selection, and request fence over primary evidence only. */
42
+ export declare function projectWakeEligibility(input: WakeSnapshot): WakeEligibility;
43
+ export declare function wakeIsEligible(evidence: WakeEvidence): boolean;
@@ -0,0 +1,106 @@
1
+ import { nameKey } from './model.js';
2
+ import { deriveDeliveryModel } from './delivery.js';
3
+ import { formatActivityId, parseActivityId } from './square-core.js';
4
+ import { decodeWakeEvidence } from './wake-evidence.js';
5
+ function identity(...parts) { return JSON.stringify(parts); }
6
+ /** One strict ownership join, latest selection, and request fence over primary evidence only. */
7
+ export function projectWakeEligibility(input) {
8
+ const delivery = input.delivery ?? deriveDeliveryModel(input.state);
9
+ const ownersBySession = new Map();
10
+ for (const owner of input.owners) {
11
+ if (owner.location !== input.location)
12
+ continue;
13
+ const key = identity(nameKey(owner.participant), owner.session);
14
+ const rows = ownersBySession.get(key) ?? [];
15
+ rows.push(owner);
16
+ ownersBySession.set(key, rows);
17
+ }
18
+ const ownerFor = (route) => route.location !== input.location ? undefined
19
+ : ownersBySession.get(identity(nameKey(route.participant), route.sessionId))?.find((row) => route.epoch === undefined || row.epoch === route.epoch);
20
+ const ownedRoutes = [];
21
+ const latest = new Map();
22
+ for (const route of input.state.routes ?? []) {
23
+ const participant = nameKey(route.participant);
24
+ const owner = ownerFor(route);
25
+ if (owner === undefined)
26
+ continue;
27
+ ownedRoutes.push({ route, owner });
28
+ latest.set(participant, Math.max(latest.get(participant) ?? Number.NEGATIVE_INFINITY, route.updatedAt));
29
+ }
30
+ const selectedByRecipient = new Map();
31
+ for (const owned of ownedRoutes) {
32
+ const key = nameKey(owned.route.participant);
33
+ if (owned.route.updatedAt !== latest.get(key))
34
+ continue;
35
+ const rows = selectedByRecipient.get(key) ?? [];
36
+ rows.push(owned);
37
+ selectedByRecipient.set(key, rows);
38
+ }
39
+ const pending = new Set();
40
+ for (const recipient of delivery.joinedRecipients()) {
41
+ for (const notification of delivery.pendingFor(recipient))
42
+ pending.add(identity(nameKey(recipient), notification.item.index));
43
+ }
44
+ const attemptsByAttention = new Map();
45
+ for (const record of input.wakeRecords) {
46
+ if (record.location !== input.location)
47
+ continue;
48
+ const decoded = decodeWakeEvidence(record, input.now);
49
+ if (decoded?.kind !== 'attempt')
50
+ continue;
51
+ const attempt = decoded.value;
52
+ const key = identity(nameKey(attempt.attention.recipient), attempt.attention.actIndex);
53
+ const rows = attemptsByAttention.get(key) ?? [];
54
+ rows.push(attempt);
55
+ attemptsByAttention.set(key, rows);
56
+ }
57
+ const presentedSessions = new Set();
58
+ for (const row of input.presentations) {
59
+ if (row.location !== input.location || (row.outcome !== 'presented' && row.outcome !== 'clipped'))
60
+ continue;
61
+ presentedSessions.add(identity(nameKey(row.participant), row.activity, row.sessionId));
62
+ }
63
+ const isPresented = (route, actIndex) => presentedSessions.has(identity(nameKey(route.participant), formatActivityId(actIndex), route.sessionId));
64
+ const isCancelled = (owner, actIndex) => actIndex <= (owner.cancelledThrough ?? -1);
65
+ return {
66
+ evidence(recipient, actIndex) {
67
+ const key = nameKey(recipient);
68
+ // Accepted/unknown exclusion belongs to attention, not to a surviving owner or route kind.
69
+ const attempts = attemptsByAttention.get(identity(key, actIndex)) ?? [];
70
+ const terminal = attempts.findLast((attempt) => attempt.outcome === 'accepted')
71
+ ?? attempts.findLast((attempt) => attempt.outcome === 'unknown');
72
+ const routes = (selectedByRecipient.get(key) ?? []).filter(({ owner }) => !isCancelled(owner, actIndex));
73
+ const presented = routes.length > 0 && routes.every(({ route }) => isPresented(route, actIndex));
74
+ return {
75
+ delivered: delivery.isSeen(recipient, actIndex), presented, attempts,
76
+ ...(terminal === undefined ? {} : { terminal }),
77
+ attemptableRoutes: terminal === undefined && pending.has(identity(key, actIndex))
78
+ ? routes.filter(({ route }) => !isPresented(route, actIndex)).map(({ route }) => route) : [],
79
+ };
80
+ },
81
+ currentness(request) {
82
+ const actIndex = parseActivityId(request.activity);
83
+ const route = request.route;
84
+ const sameRecipient = nameKey(request.participant) === nameKey(route.participant);
85
+ const sameLocation = request.location === input.location && route.location === input.location;
86
+ const activityPending = actIndex !== undefined && request.location === input.location
87
+ && pending.has(identity(nameKey(request.participant), actIndex));
88
+ const owner = sameLocation && sameRecipient ? ownerFor(route) : undefined;
89
+ const sessionBound = owner !== undefined;
90
+ const matchesRequest = (published) => published.location === route.location
91
+ && nameKey(published.participant) === nameKey(route.participant) && published.sessionId === route.sessionId
92
+ && published.kind === route.kind && published.epoch === route.epoch && Object.keys(published.address).length === Object.keys(route.address).length && Object.keys(published.address).every((key) => published.address[key] === route.address[key]);
93
+ const routePublished = sameLocation && sameRecipient && (input.state.routes ?? []).some(matchesRequest);
94
+ const selectedOwner = sameLocation && sameRecipient
95
+ && (selectedByRecipient.get(nameKey(route.participant)) ?? []).some(({ route: published }) => matchesRequest(published));
96
+ const cancelled = actIndex !== undefined && owner !== undefined && isCancelled(owner, actIndex);
97
+ const presented = actIndex !== undefined && sameLocation && sameRecipient && isPresented(route, actIndex);
98
+ // Dispatching evidence deliberately does not fence the request that owns the atomic claim.
99
+ return { current: activityPending && sessionBound && routePublished && selectedOwner && !cancelled && !presented,
100
+ activityPending, sessionBound, routePublished, selectedOwner, cancelled, presented };
101
+ },
102
+ };
103
+ }
104
+ export function wakeIsEligible(evidence) {
105
+ return !evidence.delivered && !evidence.presented && evidence.terminal === undefined && evidence.attemptableRoutes.length > 0;
106
+ }
@@ -1,9 +1,32 @@
1
- import type { SquareState } from './model.js';
2
- import { wakeIsEligible, type WakeEvidence, type WakeEvidenceProjection } from './square-projections.js';
3
- import type { DeliveryModel } from './delivery.js';
4
- export type { WakeEvidence, WakeEvidenceProjection };
5
- export { wakeIsEligible };
6
- /** Adapter entry: open the artifact, assemble host ports, and project wake evidence. */
7
- export declare function wakeEvidenceProjection(squarePath: string, now: number, env: NodeJS.ProcessEnv): Promise<WakeEvidenceProjection>;
8
- export declare function wakeEvidenceProjectionFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, delivery?: DeliveryModel): Promise<WakeEvidenceProjection>;
9
- export declare function wakeEvidence(squarePath: string, recipient: string, actIndex: number, now: number, env: NodeJS.ProcessEnv): Promise<WakeEvidence>;
1
+ import type { EvidenceRecord, WakeAttention } from './host-ledger.js';
2
+ import type { WakeRouteKind } from './model.js';
3
+ export interface WakeAttempt {
4
+ readonly at: number;
5
+ readonly attention: WakeAttention;
6
+ readonly routeKind: WakeRouteKind;
7
+ readonly outcome: 'accepted' | 'unknown' | 'failed';
8
+ readonly signature?: string;
9
+ readonly attemptN: number;
10
+ readonly session?: string;
11
+ readonly message?: string;
12
+ readonly diagnostic?: unknown;
13
+ }
14
+ export interface WakeReleaseDiagnostic {
15
+ readonly at: number;
16
+ readonly attention: WakeAttention;
17
+ readonly routeKind?: WakeRouteKind;
18
+ readonly attemptN?: number;
19
+ readonly session?: string;
20
+ readonly signature?: string;
21
+ readonly message?: string;
22
+ readonly diagnostic?: unknown;
23
+ }
24
+ export type DecodedWakeEvidence = {
25
+ readonly kind: 'attempt';
26
+ readonly value: WakeAttempt;
27
+ } | {
28
+ readonly kind: 'release';
29
+ readonly value: WakeReleaseDiagnostic;
30
+ };
31
+ /** Decode primary attempts and diagnostic-only releases without mixing their evidence. */
32
+ export declare function decodeWakeEvidence(record: EvidenceRecord, now: number): DecodedWakeEvidence | undefined;
@@ -1,26 +1,31 @@
1
- import { hostLedgerRoot } from './host-ledger-root.js';
2
- import { createHostLedgerPort } from './host-ledger-file-adapter.js';
3
- import { openSquare } from './square-file-adapter.js';
4
- import { closeOpenSquare } from './open-square.js';
5
- import { projectWakeEvidenceFromState, wakeIsEligible, } from './square-projections.js';
6
- export { wakeIsEligible };
7
- function hostLedgerForEnv(env) {
8
- return createHostLedgerPort({ rootPath: hostLedgerRoot(env) });
9
- }
10
- /** Adapter entry: open the artifact, assemble host ports, and project wake evidence. */
11
- export async function wakeEvidenceProjection(squarePath, now, env) {
12
- const square = await openSquare(squarePath, { clock: () => now, hostLedger: hostLedgerForEnv(env), env });
13
- try {
14
- const { state } = await square.artifact.read();
15
- return projectWakeEvidenceFromState({ location: squarePath, state, hostLedger: square.hostLedger, now });
16
- }
17
- finally {
18
- await closeOpenSquare(square);
1
+ import { parseActivityId } from './square-core.js';
2
+ /** Decode primary attempts and diagnostic-only releases without mixing their evidence. */
3
+ export function decodeWakeEvidence(record, now) {
4
+ if (record.kind !== 'wake')
5
+ return undefined;
6
+ const actIndex = parseActivityId(record.activity);
7
+ if (actIndex === undefined)
8
+ return undefined;
9
+ const value = {
10
+ at: record.at ?? now,
11
+ attention: { squarePath: record.location, recipient: record.participant, actIndex },
12
+ ...(record.session === undefined ? {} : { session: record.session }),
13
+ ...(record.signature === undefined ? {} : { signature: record.signature }),
14
+ ...(record.message === undefined ? {} : { message: record.message }),
15
+ ...(record.diagnostic === undefined ? {} : { diagnostic: record.diagnostic }),
16
+ };
17
+ if (record.outcome === 'released') {
18
+ if (record.at === undefined)
19
+ return undefined;
20
+ return { kind: 'release', value: {
21
+ ...value,
22
+ ...(record.routeKind === undefined ? {} : { routeKind: record.routeKind }),
23
+ ...(record.attemptN === undefined ? {} : { attemptN: record.attemptN }),
24
+ } };
19
25
  }
20
- }
21
- export function wakeEvidenceProjectionFromState(squarePath, state, now, env, delivery) {
22
- return projectWakeEvidenceFromState({ location: squarePath, state, hostLedger: hostLedgerForEnv(env), now, delivery });
23
- }
24
- export async function wakeEvidence(squarePath, recipient, actIndex, now, env) {
25
- return (await wakeEvidenceProjection(squarePath, now, env)).evidence(recipient, actIndex);
26
+ if (record.outcome !== 'accepted' && record.outcome !== 'unknown' && record.outcome !== 'failed')
27
+ return undefined;
28
+ if (record.routeKind === undefined || typeof record.attemptN !== 'number')
29
+ return undefined;
30
+ return { kind: 'attempt', value: { ...value, routeKind: record.routeKind, attemptN: record.attemptN, outcome: record.outcome } };
26
31
  }
@@ -1,14 +1,18 @@
1
+ /** One Paseo wake attempt: where it goes, what it says, and what identifies it. */
1
2
  export interface PaseoWakeRequest {
2
3
  agentId: string;
3
4
  prompt: string;
5
+ location: string;
6
+ participant: string;
7
+ activity: string;
8
+ attemptN?: number;
4
9
  }
5
- export type PaseoWakeFailureKind = 'transient' | 'rejected' | 'unknown';
6
- export declare class PaseoWakeSendError extends Error {
7
- readonly kind: PaseoWakeFailureKind;
8
- constructor(message: string, kind: PaseoWakeFailureKind);
9
- }
10
- export declare function sendPaseoWake({ agentId, prompt }: PaseoWakeRequest, opts?: {
11
- args?: string[];
12
- bin?: string;
10
+ /**
11
+ * The daemon's own dedup key for one attempt. Every retry of the same attempt
12
+ * derives the same id; a later attempt of the same attention derives a new one.
13
+ */
14
+ export declare function paseoWakeMessageId(request: Pick<PaseoWakeRequest, 'location' | 'participant' | 'activity' | 'attemptN'>): string;
15
+ /** Steer one existing Paseo agent through the shared delivery entry. Never interrupts a turn. */
16
+ export declare function sendPaseoWake(request: PaseoWakeRequest, opts?: {
13
17
  timeoutMs?: number;
14
- }): void;
18
+ }): Promise<void>;
package/dist/wake-sink.js CHANGED
@@ -1,56 +1,37 @@
1
- import { spawnSync } from 'node:child_process';
2
- export class PaseoWakeSendError extends Error {
3
- kind;
4
- constructor(message, kind) {
5
- super(message);
6
- this.kind = kind;
7
- this.name = 'PaseoWakeSendError';
8
- }
1
+ import { createHash } from 'node:crypto';
2
+ import { DeliveryError, connect } from './packages/agent-delivery/src/index.js';
3
+ /**
4
+ * The daemon's own dedup key for one attempt. Every retry of the same attempt
5
+ * derives the same id; a later attempt of the same attention derives a new one.
6
+ */
7
+ export function paseoWakeMessageId(request) {
8
+ const digest = createHash('sha256')
9
+ .update([request.location, request.participant, request.activity, request.attemptN ?? 0].join('|'))
10
+ .digest('hex');
11
+ return `square-${digest.slice(0, 32)}`;
9
12
  }
10
- function commandError(output) {
11
- try {
12
- const parsed = JSON.parse(output);
13
- if (parsed.error === undefined)
14
- return undefined;
15
- return {
16
- ...(typeof parsed.error.code === 'string' ? { code: parsed.error.code } : {}),
17
- message: typeof parsed.error.message === 'string' ? parsed.error.message : output.trim(),
18
- };
19
- }
20
- catch {
21
- return undefined;
22
- }
23
- }
24
- function classifyCommandFailure(code, message) {
25
- if (code === 'DAEMON_NOT_RUNNING' || /ECONNREFUSED|ENOENT|not found.*executable/i.test(message))
26
- return 'transient';
27
- if (/password|auth|unauthori[sz]ed|agent not found|rejected/i.test(message))
28
- return 'rejected';
29
- return 'unknown';
30
- }
31
- function redactUriPassword(value) {
32
- return value.replace(/([?&]password=)[^&\s]+/gi, '$1[redacted]');
33
- }
34
- function configuredArguments(name) {
35
- try {
36
- const parsed = JSON.parse(process.env[name] ?? '[]');
37
- return Array.isArray(parsed) && parsed.every((item) => typeof item === 'string') ? parsed : [];
38
- }
39
- catch {
40
- return [];
41
- }
42
- }
43
- export function sendPaseoWake({ agentId, prompt }, opts = {}) {
44
- const result = spawnSync(opts.bin ?? process.env.SQUARE_PASEO_BIN ?? 'paseo', [...(opts.args ?? configuredArguments('SQUARE_PASEO_BIN_ARGS')), 'send', agentId, '--prompt', prompt, '--no-wait', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: opts.timeoutMs ?? 5000, env: process.env });
45
- if (result.error) {
46
- const code = result.error.code;
47
- const kind = code === 'ENOENT' || code === 'ECONNREFUSED' ? 'transient' : 'unknown';
48
- throw new PaseoWakeSendError(result.error.message, kind);
49
- }
50
- if (result.status === 0)
51
- return;
52
- const output = `${result.stderr ?? ''}${result.stdout ?? ''}`;
53
- const failure = commandError(output);
54
- const message = redactUriPassword(failure?.message || output.trim() || `paseo send exited with ${result.status ?? 'no status'}`);
55
- throw new PaseoWakeSendError(message, classifyCommandFailure(failure?.code, message));
13
+ /** Steer one existing Paseo agent through the shared delivery entry. Never interrupts a turn. */
14
+ export async function sendPaseoWake(request, opts = {}) {
15
+ const budgetMs = opts.timeoutMs;
16
+ // A usable budget becomes one deadline both phases share; anything else is an
17
+ // argument error, so it is handed to the entry untouched to classify.
18
+ const bounded = budgetMs !== undefined && Number.isFinite(budgetMs) && budgetMs > 0;
19
+ const deadline = bounded ? Date.now() + budgetMs : 0;
20
+ // An elapsed budget never reached the daemon, so it is the shared entry's own
21
+ // "unavailable" failure rather than a send error of its own.
22
+ const connectMs = bounded ? Math.max(0, deadline - Date.now()) : budgetMs;
23
+ if (bounded && connectMs === 0)
24
+ throw new DeliveryError('unavailable');
25
+ const agent = await connect({
26
+ harness: 'paseo',
27
+ agentId: request.agentId,
28
+ ...(connectMs === undefined ? {} : { timeoutMs: connectMs }),
29
+ });
30
+ const steerMs = bounded ? Math.max(0, deadline - Date.now()) : budgetMs;
31
+ if (bounded && steerMs === 0)
32
+ throw new DeliveryError('unavailable');
33
+ await agent.steer(request.prompt, {
34
+ id: paseoWakeMessageId(request),
35
+ ...(steerMs === undefined ? {} : { timeoutMs: steerMs }),
36
+ });
56
37
  }
package/dist/watch.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isSquareError, } from './model.js';
2
- import { STALE_MS, WATCH_HEARTBEAT_MS, inSquareCount, nowMs, } from './runtime.js';
2
+ import { STALE_MS, WATCH_HEARTBEAT_MS, countSays, inSquareCount, nowMs, } from './runtime.js';
3
3
  import { openSquare } from './square-file-adapter.js';
4
4
  import { closeOpenSquare } from './open-square.js';
5
5
  import { createSquareApplication } from './square-application.js';
@@ -20,20 +20,14 @@ function watchOutputResult(squarePath, presentation, name, caught, opts = {}) {
20
20
  const stored = index === undefined ? undefined : presentation.activities.find((item) => item.index === index);
21
21
  return stored === undefined ? [] : [{ activity: stored, perception: activity.perception }];
22
22
  });
23
- const publicItems = delivered
24
- .map(({ activity }) => activity)
25
- .filter((item) => item.kind === 'say' || item.kind === 'done');
26
- const roomChanges = delivered
27
- .map(({ activity }) => activity)
28
- .filter((item) => item.kind === 'join' || item.kind === 'done' || item.kind === 'hold' || item.kind === 'resume');
23
+ // Catch delivers only directed say activities, so there is no lifecycle list to split off.
24
+ const publicItems = delivered.map(({ activity }) => activity).filter((item) => item.kind === 'say' || item.kind === 'done');
29
25
  const perceptions = new Map(delivered.map(({ activity, perception }) => [activity.index, perception]));
30
26
  return {
31
27
  type: 'output',
32
- stdout: renderWatchOutput([...presentation.activities], publicItems, roomChanges, {
28
+ stdout: renderWatchOutput([...presentation.activities], publicItems, {
33
29
  ...opts,
34
- squarePath,
35
30
  viewer: name,
36
- showCatchHint: !hasAutomaticDeliveryIdentity(),
37
31
  perceptions,
38
32
  }) + (caught.remaining > 0
39
33
  ? `\n○ ${caught.remaining} matching ${caught.remaining === 1 ? 'activity remains' : 'activities remain'}\n${catchContinuationCommand(squarePath, name, opts)}`
@@ -72,6 +66,8 @@ function writeWatchTerminal(squarePath, name, presentation, status, idleMs) {
72
66
  ...(idleMs === undefined ? {} : { idleMs }),
73
67
  presence: presentation.presence,
74
68
  showCatchHint: !hasAutomaticDeliveryIdentity(),
69
+ ownActivityCount: countSays(presentation.state.acts, name),
70
+ hardCap: presentation.state.hardCap,
75
71
  }), { participantCount: presentation.participantCount }));
76
72
  }
77
73
  function writeWatchReplaced(squarePath, name, presentation) {
@@ -142,14 +138,6 @@ async function cmdWatchNow(squarePath, name, opts, caller) {
142
138
  await closeOpenSquare(square);
143
139
  }
144
140
  }
145
- async function countParticipants(square) {
146
- try {
147
- return inSquareCount((await square.artifact.read()).state);
148
- }
149
- catch {
150
- return undefined;
151
- }
152
- }
153
141
  /** `false` is reserved for a quiet --now; idle completion preserves its existing sweep boundary. */
154
142
  export async function cmdWatch(squarePath, name, opts, caller = { cwd: process.cwd(), env: { ...process.env } }) {
155
143
  let square;
@@ -163,7 +151,7 @@ export async function cmdWatch(squarePath, name, opts, caller = { cwd: process.c
163
151
  if (isSquareError(err)) {
164
152
  const bodyLines = [err.message];
165
153
  // The caller's own name has never joined; only a join admits it.
166
- if (/^✕ .* has never stepped into/.test(err.message))
154
+ if (err.facts?.reason === 'never_joined')
167
155
  bodyLines.push(joinRecoveryCommand(squarePath, name));
168
156
  process.stderr.write(formatRefusal(squarePath, bodyLines, participantCount === undefined ? {} : { participantCount }));
169
157
  process.exit(err.code === 'not_found' ? 1 : 2);
@@ -178,12 +166,16 @@ export async function cmdWatch(squarePath, name, opts, caller = { cwd: process.c
178
166
  }
179
167
  }
180
168
  catch (err) {
181
- const participantCount = await countParticipants(square);
169
+ let participantCount;
170
+ try {
171
+ participantCount = inSquareCount((await square.artifact.read()).state);
172
+ }
173
+ catch { /* the header count is best effort */ }
182
174
  await closeOpenSquare(square).catch(() => undefined);
183
175
  if (isSquareError(err)) {
184
176
  const bodyLines = [err.message];
185
177
  // A filter target is unknown, not the caller; the roster is the useful next read.
186
- if (/^✕ .*(has never stepped into|is not standing in this square)/.test(err.message))
178
+ if (err.facts?.reason === 'never_joined' || err.facts?.reason === 'not_standing')
187
179
  bodyLines.push(participantsRecoveryCommand(squarePath));
188
180
  process.stderr.write(formatRefusal(squarePath, bodyLines, participantCount === undefined ? {} : { participantCount }));
189
181
  process.exit(err.code === 'not_found' ? 1 : 2);
@@ -2,6 +2,7 @@ import { presentPendingAtBoundary, renderPendingAtBoundary } from '../dist/bound
2
2
  import { automaticSessionEnd, automaticSessionStart } from '../dist/automatic-session.js';
3
3
  import { sessionInbox, observeSessionPending } from '../dist/inbox.js';
4
4
  import { renderAttentionDescription } from '../dist/attention-presentation.js';
5
+ import { createPiReceiver, sendPiMessage } from '../dist/packages/agent-delivery/src/pi.js';
5
6
 
6
7
  class PiDeliveryDroppedError extends Error {
7
8
  constructor() {
@@ -42,6 +43,10 @@ export function summarizePendingForNotify(pending) {
42
43
  }
43
44
 
44
45
  export default function squarePiExtension(pi) {
46
+ pi.registerFlag('agent-delivery-socket', { type: 'string', description: 'Explicit private local delivery socket' });
47
+ createPiReceiver(pi, {
48
+ get endpoint() { return pi.getFlag('agent-delivery-socket'); },
49
+ });
45
50
  let sessionId;
46
51
  let sessionCwd;
47
52
  let ui;
@@ -115,12 +120,12 @@ export default function squarePiExtension(pi) {
115
120
  // Pi owns PI_SESSION_ID and several SDK sessions may coexist in one process.
116
121
  const sessionEnv = (id) => ({ ...process.env, ...(id === undefined ? {} : { PI_SESSION_ID: id }) });
117
122
 
118
- // A transport may keep its promise pending while Pi is shutting down or
119
- // replacing a session. Never make a lifecycle hook wait for that transport.
123
+ // Aborting releases native landing waits; retain cleanup until the observer
124
+ // has closed, including when cancellation precedes a lifecycle hook.
120
125
  const stopWatcher = () => {
121
126
  watcherAbort?.abort();
122
- watcher = undefined;
123
127
  watcherAbort = undefined;
128
+ return watcher;
124
129
  };
125
130
 
126
131
  const pause = (signal, delayMs) => new Promise((resolve) => {
@@ -201,10 +206,10 @@ export default function squarePiExtension(pi) {
201
206
  const messageContent = framePiMessage(content);
202
207
  const landing = waitForLanding(messageContent, signal, 'steer');
203
208
  try {
204
- Promise.resolve(pi.sendMessage(
209
+ sendPiMessage(pi,
205
210
  { customType: 'square', content: messageContent, display: false },
206
211
  { deliverAs: 'steer', triggerTurn: true },
207
- )).catch((error) => landing.ack.settle(error));
212
+ );
208
213
  } catch (error) {
209
214
  landing.ack.settle(error);
210
215
  }
@@ -224,6 +229,7 @@ export default function squarePiExtension(pi) {
224
229
  env,
225
230
  signal,
226
231
  );
232
+ if (signal.aborted || token !== generation) return;
227
233
  for (const key of keys) handledPending.add(key);
228
234
  } catch (error) {
229
235
  if (signal.aborted || token !== generation) return;
@@ -247,7 +253,7 @@ export default function squarePiExtension(pi) {
247
253
  cancelledRun = false;
248
254
  generation += 1;
249
255
  failAcks(new Error('Pi session replaced'));
250
- stopWatcher();
256
+ const stopped = stopWatcher();
251
257
  handledPending.clear();
252
258
  observedPending.clear();
253
259
  retryAfterChange = false;
@@ -259,29 +265,31 @@ export default function squarePiExtension(pi) {
259
265
  sessionId = ctx.sessionManager.getSessionId();
260
266
  sessionCwd = ctx.cwd || process.cwd();
261
267
  const token = generation;
268
+ await stopped;
269
+ if (token !== generation) return;
262
270
  watcherAbort = new AbortController();
263
271
  const signal = watcherAbort.signal;
264
- void automaticSessionStart('pi', sessionId, sessionCwd, sessionEnv(sessionId)).then((context) => {
272
+ const startup = automaticSessionStart('pi', sessionId, sessionCwd, sessionEnv(sessionId)).then((context) => {
265
273
  if (context === undefined || sessionId === undefined || token !== generation) return;
266
274
  const messageContent = framePiMessage(context);
267
275
  const landing = waitForLanding(messageContent, signal, 'nextTurn');
268
276
  try {
269
- Promise.resolve(pi.sendMessage(
277
+ sendPiMessage(pi,
270
278
  { customType: 'square', content: messageContent, display: true },
271
279
  { deliverAs: 'nextTurn' },
272
- )).catch((error) => landing.ack.settle(error));
280
+ );
273
281
  } catch {
274
282
  // Joining context is advisory; an unavailable Pi transport does not block startup.
275
283
  landing.ack.settle(new Error('Pi joining-context injection failed'));
276
284
  }
277
285
  void landing.promise.catch(() => undefined);
278
286
  }).catch(() => undefined);
279
- watcher = wake(token, signal).catch(() => undefined);
287
+ watcher = Promise.allSettled([startup, wake(token, signal)]).then(() => undefined);
280
288
  });
281
289
 
282
290
  pi.on('agent_start', async (_event, ctx) => {
283
291
  detachRunAbort?.();
284
- currentRunSignal = ctx?.getSignal?.();
292
+ currentRunSignal = ctx?.signal;
285
293
  if (ctx?.mode !== 'tui' || currentRunSignal === undefined) return;
286
294
  const runSignal = currentRunSignal;
287
295
  detachRunAbort = () => runSignal.removeEventListener('abort', cancelRunDelivery);
@@ -329,6 +337,8 @@ export default function squarePiExtension(pi) {
329
337
  const token = generation;
330
338
  // Cancel the entire pending batch, including entries not yet sent to Pi.
331
339
  // This is local suppression, never evidence that the model saw the activity.
340
+ await stopWatcher();
341
+ if (token !== generation || sessionId === undefined) return;
332
342
  const cancelled = await sessionInbox(sessionId, sessionEnv(sessionId));
333
343
  if (token !== generation || sessionId === undefined) return;
334
344
  for (const key of inboxKeys(cancelled)) handledPending.add(key);
@@ -343,13 +353,17 @@ export default function squarePiExtension(pi) {
343
353
  cancelledRun = false;
344
354
  generation += 1;
345
355
  failAcks(new Error('Pi session ended'));
346
- stopWatcher();
356
+ const stopped = stopWatcher();
347
357
  ui = undefined;
348
- if (sessionId && sessionCwd) void automaticSessionEnd('pi', sessionId, sessionCwd, sessionEnv(sessionId)).catch(() => undefined);
358
+ const endedId = sessionId;
359
+ const endedCwd = sessionCwd;
360
+ const endedEnv = sessionEnv(endedId);
349
361
  sessionId = undefined;
350
362
  sessionCwd = undefined;
351
363
  retryWait = undefined;
352
364
  activeTurn = undefined;
353
365
  currentRunSignal = undefined;
366
+ await stopped;
367
+ if (endedId && endedCwd) await automaticSessionEnd('pi', endedId, endedCwd, endedEnv).catch(() => undefined);
354
368
  });
355
369
  }
@@ -4,7 +4,7 @@ Square's artifact and host-ledger adapters own change detection. Notifications a
4
4
 
5
5
  - A process shares a revision reader per canonical artifact and a filesystem watcher per containing directory. Subscribers have independent baselines, deadlines, and cancellation. The final release closes the watcher, reconciliation timer, and any outstanding read.
6
6
  - Directory notifications invalidate lightweight reads. A two-second reconciliation checks revisions/ledger metadata even when notifications are missing. Missing directories, unsupported watches, and watcher failures retain that fallback; directory replacement is detected during reconciliation. Normal notification latency is not a hard guarantee: a missed hint may wait for reconciliation, plus scheduling and lock contention.
7
- - The artifact revision remains authoritative. No new sidecar, broker, or persistent cache is introduced. Only changed projections load historical state. Ledger metadata invalidates bindings and presentation projections; it is never treated as evidence of delivery.
7
+ - The artifact revision remains authoritative. No new sidecar, broker, or persistent cache is introduced. Only changed projections load historical state. A missing artifact is a stable observation, not a repeated change: reconciliation remains quiet while it stays absent and detects its return without deleting its session bindings. Strict artifact reads still report absence as an error. Ledger metadata invalidates bindings and presentation projections; it is never treated as evidence of delivery.
8
8
  - Each Pi session owns a long-lived pending observer, including when it has no bindings yet. Presence and evidence changes update its subscriptions. There is no repeated thirty-second restart and no per-second binding scan. A failed injection waits for a substantive change; its own failed evidence write does not trigger a retry loop.
9
9
  - Blocking CLI catch waits for a revision or its next heartbeat/quiet deadline. A held catch keeps its lease alive and pauses quiet expiry. Core catch, NDJSON stream, and delivery-receipt waits use the same change detector. Held express also waits for changes; throttling still waits for its eligibility time.
10
10
  - Catch lease expiry, ledger retention/claim expiry, and cancellation are independent wake conditions. No filesystem write is required for a time-based eligibility change.