@north-light/crouter 0.3.220 → 0.3.221

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 (148) hide show
  1. package/dist/api/client.d.ts +9 -0
  2. package/dist/api/client.js +10 -0
  3. package/dist/api/dto/chat-inventory.d.ts +43 -0
  4. package/dist/api/dto/chat-inventory.js +11 -0
  5. package/dist/api/dto/profiles.d.ts +19 -5
  6. package/dist/api/dto/profiles.js +2 -1
  7. package/dist/api/index.d.ts +1 -0
  8. package/dist/api/index.js +1 -0
  9. package/dist/api/routes.d.ts +1 -0
  10. package/dist/api/routes.js +1 -0
  11. package/dist/build-root.d.ts +2 -6
  12. package/dist/build-root.js +51 -4
  13. package/dist/builtin-memory/internal/agent-shaping.md +3 -1
  14. package/dist/builtin-memory/internal/memory-loading.md +4 -0
  15. package/dist/builtin-memory/plan/roadmap.md +7 -1
  16. package/dist/builtin-memory/spec/guide.md +7 -1
  17. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +7 -2
  20. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
  21. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
  22. package/dist/clients/attach/overlays/file-review.js +2 -2
  23. package/dist/clients/attach/session/keys.d.ts +1 -1
  24. package/dist/clients/attach/session/profile-files.js +1 -1
  25. package/dist/clients/attach/viewer.js +690 -690
  26. package/dist/clients/inbox/review/launch.d.ts +8 -4
  27. package/dist/clients/inbox/review/launch.js +55 -5
  28. package/dist/clients/inbox/review/review-client.d.ts +1 -0
  29. package/dist/clients/inbox/review/review-client.js +4 -0
  30. package/dist/clients/inbox/review-adapter.d.ts +1 -8
  31. package/dist/clients/inbox/review-adapter.js +4 -52
  32. package/dist/commands/memory/lint.js +2 -1
  33. package/dist/commands/memory/read.js +1 -0
  34. package/dist/commands/memory.js +1 -1
  35. package/dist/commands/pkg/market-manage.js +165 -75
  36. package/dist/commands/pkg/plugin-inspect.js +19 -2
  37. package/dist/commands/pkg/plugin-manage.d.ts +8 -3
  38. package/dist/commands/pkg/plugin-manage.js +72 -24
  39. package/dist/commands/profile/default.js +6 -10
  40. package/dist/commands/profile/list.js +5 -3
  41. package/dist/commands/profile/new.js +21 -8
  42. package/dist/commands/profile/project.js +25 -19
  43. package/dist/commands/profile/show.js +3 -3
  44. package/dist/commands/surface-inbox.js +1 -0
  45. package/dist/commands/sys/__tests__/migrate.test.js +16 -5
  46. package/dist/commands/sys/doctor.js +35 -5
  47. package/dist/commands/sys/migrate.js +38 -19
  48. package/dist/commands/sys/setup-core.js +1 -1
  49. package/dist/commands/sys/sync-project-guidance.js +1 -1
  50. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
  51. package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
  52. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
  53. package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
  54. package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
  55. package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
  56. package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
  57. package/dist/core/bootstrap.js +6 -0
  58. package/dist/core/canvas/browse/app.js +5 -2
  59. package/dist/core/canvas/browse/model.d.ts +25 -15
  60. package/dist/core/canvas/browse/model.js +86 -65
  61. package/dist/core/canvas/render-source.d.ts +6 -0
  62. package/dist/core/canvas/render-source.js +7 -1
  63. package/dist/core/canvas/render.js +10 -2
  64. package/dist/core/command-hooks/artifact.d.ts +10 -0
  65. package/dist/core/command-hooks/artifact.js +129 -0
  66. package/dist/core/command-hooks/catalog.d.ts +14 -0
  67. package/dist/core/command-hooks/catalog.js +38 -0
  68. package/dist/core/command-hooks/compose.d.ts +15 -0
  69. package/dist/core/command-hooks/compose.js +99 -0
  70. package/dist/core/command-hooks/discovery.d.ts +87 -0
  71. package/dist/core/command-hooks/discovery.js +174 -0
  72. package/dist/core/command-hooks/help.d.ts +5 -0
  73. package/dist/core/command-hooks/help.js +18 -0
  74. package/dist/core/command-hooks/index.d.ts +6 -0
  75. package/dist/core/command-hooks/index.js +6 -0
  76. package/dist/core/command-hooks/report.d.ts +23 -0
  77. package/dist/core/command-hooks/report.js +19 -0
  78. package/dist/core/command-hooks/schema.d.ts +27 -0
  79. package/dist/core/command-hooks/schema.js +68 -0
  80. package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
  81. package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
  82. package/dist/core/command-plugins/presence.d.ts +2 -0
  83. package/dist/core/command-plugins/presence.js +17 -0
  84. package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
  85. package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
  86. package/dist/core/command.d.ts +8 -1
  87. package/dist/core/command.js +12 -10
  88. package/dist/core/help.d.ts +7 -1
  89. package/dist/core/io.d.ts +9 -1
  90. package/dist/core/io.js +44 -2
  91. package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
  92. package/dist/core/memory/inline-ref-inventory.js +15 -8
  93. package/dist/core/memory-resolver.d.ts +13 -1
  94. package/dist/core/memory-resolver.js +25 -19
  95. package/dist/core/profiles/manifest.d.ts +13 -2
  96. package/dist/core/profiles/manifest.js +84 -18
  97. package/dist/core/profiles/select.js +9 -9
  98. package/dist/core/render.js +11 -0
  99. package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
  100. package/dist/core/runtime/advertised-command-invocation.js +233 -0
  101. package/dist/core/runtime/bearings.js +1 -1
  102. package/dist/core/runtime/broker/event-projection.js +7 -0
  103. package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
  104. package/dist/core/runtime/broker/frame-dispatch.js +15 -10
  105. package/dist/core/runtime/broker/read-ops.d.ts +4 -0
  106. package/dist/core/runtime/broker/read-ops.js +6 -2
  107. package/dist/core/runtime/broker-extension-render.js +1 -1
  108. package/dist/core/runtime/broker-inventory.d.ts +5 -0
  109. package/dist/core/runtime/broker-inventory.js +191 -0
  110. package/dist/core/runtime/broker-protocol.d.ts +9 -2
  111. package/dist/core/runtime/broker.js +10 -1
  112. package/dist/core/runtime/command-surface.d.ts +33 -0
  113. package/dist/core/runtime/command-surface.js +81 -0
  114. package/dist/core/runtime/node-read.js +5 -0
  115. package/dist/core/scope.d.ts +26 -1
  116. package/dist/core/scope.js +52 -12
  117. package/dist/core/substrate/on-read.d.ts +7 -1
  118. package/dist/core/substrate/on-read.js +13 -4
  119. package/dist/core/substrate/render.js +14 -5
  120. package/dist/core/substrate/schema.d.ts +11 -1
  121. package/dist/core/substrate/schema.js +11 -2
  122. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +4 -4
  123. package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
  124. package/dist/daemon/api/handlers/chat-inventory.js +25 -0
  125. package/dist/daemon/api/handlers/profiles.js +7 -1
  126. package/dist/daemon/api/map.d.ts +2 -1
  127. package/dist/daemon/api/map.js +3 -2
  128. package/dist/daemon/api/server.js +2 -0
  129. package/dist/hook-authoring.d.ts +75 -0
  130. package/dist/hook-authoring.js +358 -0
  131. package/dist/hook-process.d.ts +7 -0
  132. package/dist/hook-process.js +34 -0
  133. package/dist/index.d.ts +2 -0
  134. package/dist/index.js +2 -0
  135. package/dist/migrations/002-profile-project-memory.d.ts +2 -0
  136. package/dist/migrations/002-profile-project-memory.js +71 -0
  137. package/dist/migrations/profile-manifests.d.ts +30 -0
  138. package/dist/migrations/profile-manifests.js +70 -0
  139. package/dist/migrations/registry.js +10 -5
  140. package/dist/migrations/types.d.ts +28 -1
  141. package/dist/migrations/types.js +15 -9
  142. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
  143. package/dist/pi-extensions/canvas-structured-output.js +85 -2
  144. package/dist/types.d.ts +15 -6
  145. package/package.json +1 -1
  146. package/runtime.lock.json +2 -2
  147. package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
  148. /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/profile-project-memory-delivery.test.d.ts} +0 -0
@@ -32,6 +32,13 @@ function scalarStr(v) {
32
32
  return JSON.stringify(v);
33
33
  return String(v);
34
34
  }
35
+ function fencedJson(value) {
36
+ const json = JSON.stringify(value, null, 2);
37
+ let fence = '```';
38
+ while (json.includes(fence))
39
+ fence += '`';
40
+ return `${fence}json\n${json}\n${fence}`;
41
+ }
35
42
  /** A string field is prose when it is named as such, spans lines, or is long
36
43
  * enough that it reads as a sentence rather than an identifier. */
37
44
  function isProse(name, v) {
@@ -122,6 +129,10 @@ export function renderError(p) {
122
129
  // the value the caller supplied.
123
130
  if (p.http_status !== undefined)
124
131
  lines.push(`http status: ${p.http_status}`);
132
+ if ('primaryCompleted' in p && p.primaryCompleted === true) {
133
+ lines.push('primaryCompleted: true');
134
+ lines.push(`Primary result (the command already completed):\n${fencedJson(p.result)}`);
135
+ }
125
136
  lines.push(`Next: ${p.next}`);
126
137
  return `<error code="${attrEsc(p.error)}">\n${lines.join('\n')}\n</error>`;
127
138
  }
@@ -0,0 +1,20 @@
1
+ import type { BrokerSession } from './broker/read-ops.js';
2
+ export interface AdvertisedCommandInvocationMetadata {
3
+ crouterAdvertisedCommandInvocation: {
4
+ raw: string;
5
+ };
6
+ }
7
+ /** Restore extension-owned details while projecting persisted invocation
8
+ * metadata onto the runtime custom message. */
9
+ export declare function hydratePersistedAdvertisedCommandMessages(messages: readonly unknown[]): void;
10
+ /** Add live invocation metadata without changing extension-owned content or
11
+ * details. EventProjection calls this before relaying a custom message. */
12
+ export declare function hydrateLiveAdvertisedCommandMessage(message: unknown): void;
13
+ /** Bind the imminent message_end persistence call to the exact queued app
14
+ * message rather than to a payload-equality search across pending queues. */
15
+ export declare function prepareAdvertisedCommandPersistence(session: BrokerSession, message: unknown): void;
16
+ /** Install persistence/reload plumbing once on a newly built broker session. */
17
+ export declare function installAdvertisedCommandInvocationContract(session: BrokerSession): void;
18
+ /** Preserve a chat-advertised extension command's exact submitted text on the
19
+ * first displayed custom message its handler persists. */
20
+ export declare function promptWithAdvertisedCommandInvocation<T>(session: BrokerSession, raw: string, runPrompt: () => Promise<T>): Promise<T>;
@@ -0,0 +1,233 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { registrationChatSurface } from './command-surface.js';
3
+ const advertisedCommandInvocation = new AsyncLocalStorage();
4
+ const advertisedCustomMessage = new AsyncLocalStorage();
5
+ const invocationAwareSessions = new WeakSet();
6
+ const persistedRuntimeMessages = new WeakSet();
7
+ const endingPersistence = new WeakMap();
8
+ function persistenceDetails(target) {
9
+ return {
10
+ crouterPersistenceEnvelope: true,
11
+ crouterAdvertisedCommandInvocation: { raw: target.raw },
12
+ ...(target.details === undefined
13
+ ? {}
14
+ : { extensionDetails: target.details }),
15
+ };
16
+ }
17
+ function persistedDetails(value) {
18
+ if (typeof value !== 'object' || value === null)
19
+ return null;
20
+ const details = value;
21
+ return details.crouterPersistenceEnvelope === true &&
22
+ typeof details.crouterAdvertisedCommandInvocation?.raw === 'string'
23
+ ? details
24
+ : null;
25
+ }
26
+ function matchesTarget(target, message) {
27
+ const contentMatches = message.content === target.content ||
28
+ (target.content == null &&
29
+ Array.isArray(message.content) &&
30
+ message.content.length === 0);
31
+ return message.customType === target.customType &&
32
+ contentMatches &&
33
+ message.details === target.details &&
34
+ message.display === true;
35
+ }
36
+ function hydratePersistedMessage(message) {
37
+ if (typeof message !== 'object' || message === null)
38
+ return;
39
+ const record = message;
40
+ if (record['role'] !== 'custom')
41
+ return;
42
+ const persisted = persistedDetails(record['details']);
43
+ if (persisted === null)
44
+ return;
45
+ record['details'] = persisted.extensionDetails;
46
+ record['crouterAdvertisedCommandInvocation'] =
47
+ persisted.crouterAdvertisedCommandInvocation;
48
+ }
49
+ /** Restore extension-owned details while projecting persisted invocation
50
+ * metadata onto the runtime custom message. */
51
+ export function hydratePersistedAdvertisedCommandMessages(messages) {
52
+ for (const message of messages)
53
+ hydratePersistedMessage(message);
54
+ }
55
+ /** Add live invocation metadata without changing extension-owned content or
56
+ * details. EventProjection calls this before relaying a custom message. */
57
+ export function hydrateLiveAdvertisedCommandMessage(message) {
58
+ if (typeof message !== 'object' || message === null)
59
+ return;
60
+ const record = message;
61
+ if (record['role'] !== 'custom')
62
+ return;
63
+ if (typeof record['crouterAdvertisedCommandInvocation']?.raw === 'string') {
64
+ return;
65
+ }
66
+ const target = advertisedCustomMessage.getStore();
67
+ if (target === undefined ||
68
+ target.relayed ||
69
+ !matchesTarget(target, record)) {
70
+ return;
71
+ }
72
+ target.relayed = true;
73
+ record['crouterAdvertisedCommandInvocation'] = { raw: target.raw };
74
+ if (target.persisted)
75
+ persistedRuntimeMessages.add(record);
76
+ }
77
+ /** Bind the imminent message_end persistence call to the exact queued app
78
+ * message rather than to a payload-equality search across pending queues. */
79
+ export function prepareAdvertisedCommandPersistence(session, message) {
80
+ if (typeof message !== 'object' ||
81
+ message === null ||
82
+ persistedRuntimeMessages.has(message)) {
83
+ return;
84
+ }
85
+ const record = message;
86
+ if (record['role'] !== 'custom' || record['display'] !== true)
87
+ return;
88
+ const metadata = record['crouterAdvertisedCommandInvocation'];
89
+ if (typeof metadata?.raw !== 'string')
90
+ return;
91
+ endingPersistence.set(session.sessionManager, {
92
+ raw: metadata.raw,
93
+ customType: String(record['customType'] ?? ''),
94
+ content: record['content'],
95
+ details: record['details'],
96
+ persisted: false,
97
+ relayed: true,
98
+ });
99
+ }
100
+ /** Install persistence/reload plumbing once on a newly built broker session. */
101
+ export function installAdvertisedCommandInvocationContract(session) {
102
+ if (invocationAwareSessions.has(session))
103
+ return;
104
+ invocationAwareSessions.add(session);
105
+ const pendingPersistence = [];
106
+ const manager = session.sessionManager;
107
+ const appendCustomMessageEntry = manager.appendCustomMessageEntry.bind(manager);
108
+ manager.appendCustomMessageEntry = (customType, content, display, details) => {
109
+ const current = advertisedCustomMessage.getStore();
110
+ const ending = endingPersistence.get(manager);
111
+ const target = ending !== undefined &&
112
+ matchesTarget(ending, { customType, content, display, details })
113
+ ? ending
114
+ : current !== undefined &&
115
+ !current.persisted &&
116
+ matchesTarget(current, { customType, content, display, details })
117
+ ? current
118
+ : pendingPersistence.find((candidate) => !candidate.persisted &&
119
+ matchesTarget(candidate, { customType, content, display, details }));
120
+ if (target === undefined) {
121
+ return appendCustomMessageEntry(customType, content, display, details);
122
+ }
123
+ target.persisted = true;
124
+ if (target === ending) {
125
+ endingPersistence.delete(manager);
126
+ const queuedIndex = pendingPersistence.findIndex((candidate) => candidate.raw === target.raw &&
127
+ matchesTarget(candidate, { customType, content, display, details }));
128
+ if (queuedIndex !== -1)
129
+ pendingPersistence.splice(queuedIndex, 1);
130
+ }
131
+ else {
132
+ const pendingIndex = pendingPersistence.indexOf(target);
133
+ if (pendingIndex !== -1)
134
+ pendingPersistence.splice(pendingIndex, 1);
135
+ }
136
+ return appendCustomMessageEntry(customType, content, display, persistenceDetails(target));
137
+ };
138
+ const buildSessionContext = manager.buildSessionContext.bind(manager);
139
+ manager.buildSessionContext = () => {
140
+ const context = buildSessionContext();
141
+ hydratePersistedAdvertisedCommandMessages(context.messages);
142
+ return context;
143
+ };
144
+ hydratePersistedAdvertisedCommandMessages(session.messages);
145
+ // `.agent` is optional on a session — the fake-engine fixture has none, and
146
+ // every other consumer (rebind, frame-dispatch) guards the same way.
147
+ const agent = session.agent;
148
+ for (const method of agent === undefined ? [] : ['steer', 'followUp']) {
149
+ const original = agent?.[method];
150
+ if (typeof original !== 'function' || agent === undefined)
151
+ continue;
152
+ agent[method] = (message, ...args) => {
153
+ const target = advertisedCustomMessage.getStore();
154
+ if (target !== undefined &&
155
+ typeof message === 'object' &&
156
+ message !== null &&
157
+ matchesTarget(target, message)) {
158
+ hydrateLiveAdvertisedCommandMessage(message);
159
+ if (!target.persisted && !pendingPersistence.includes(target)) {
160
+ pendingPersistence.push(target);
161
+ }
162
+ }
163
+ return original.call(agent, message, ...args);
164
+ };
165
+ }
166
+ const sendCustomMessage = session.sendCustomMessage.bind(session);
167
+ session.sendCustomMessage = (message, options) => {
168
+ const invocation = advertisedCommandInvocation.getStore();
169
+ if (invocation === undefined ||
170
+ !invocation.active ||
171
+ invocation.claimed ||
172
+ message.display !== true) {
173
+ return sendCustomMessage(message, options);
174
+ }
175
+ invocation.claimed = true;
176
+ const target = {
177
+ raw: invocation.raw,
178
+ customType: message.customType,
179
+ content: message.content,
180
+ details: message.details,
181
+ persisted: false,
182
+ relayed: false,
183
+ };
184
+ return advertisedCustomMessage.run(target, () => {
185
+ const result = sendCustomMessage(message, options);
186
+ if (options?.deliverAs === 'nextTurn') {
187
+ const pending = session._pendingNextTurnMessages;
188
+ if (Array.isArray(pending)) {
189
+ for (let index = pending.length - 1; index >= 0; index -= 1) {
190
+ const queued = pending[index];
191
+ if (typeof queued !== 'object' ||
192
+ queued === null ||
193
+ !matchesTarget(target, queued)) {
194
+ continue;
195
+ }
196
+ hydrateLiveAdvertisedCommandMessage(queued);
197
+ pendingPersistence.push(target);
198
+ break;
199
+ }
200
+ }
201
+ }
202
+ return result;
203
+ });
204
+ };
205
+ }
206
+ /** Preserve a chat-advertised extension command's exact submitted text on the
207
+ * first displayed custom message its handler persists. */
208
+ export function promptWithAdvertisedCommandInvocation(session, raw, runPrompt) {
209
+ if (!raw.startsWith('/'))
210
+ return runPrompt();
211
+ const spaceIndex = raw.indexOf(' ');
212
+ const commandName = spaceIndex === -1 ? raw.slice(1) : raw.slice(1, spaceIndex);
213
+ let command;
214
+ try {
215
+ command = session.extensionRunner?.getCommand(commandName);
216
+ }
217
+ catch {
218
+ return runPrompt();
219
+ }
220
+ if (!registrationChatSurface(command).gatewayCapable)
221
+ return runPrompt();
222
+ installAdvertisedCommandInvocationContract(session);
223
+ const invocation = { raw, claimed: false, active: true };
224
+ return advertisedCommandInvocation.run(invocation, async () => {
225
+ try {
226
+ return await runPrompt();
227
+ }
228
+ finally {
229
+ // Unawaited descendants are no longer part of the command outcome.
230
+ invocation.active = false;
231
+ }
232
+ });
233
+ }
@@ -164,7 +164,7 @@ export function profileNote(nodeId) {
164
164
  catch {
165
165
  return '';
166
166
  }
167
- const projects = entry.manifest.projects;
167
+ const projects = entry.manifest.projects.map((project) => project.path);
168
168
  if (projects.length === 0)
169
169
  return '';
170
170
  return (`Your working dir is \`${meta.cwd}\`. Your profile **${entry.manifest.name}** also has purview ` +
@@ -3,6 +3,7 @@ import { emitEvent } from '../../events/emit.js';
3
3
  import { releaseKnownStreamWait, resolveStreamWatchdogMs, StreamWatchdog } from '../stream-watchdog.js';
4
4
  import { withoutYieldAbort } from '../session-cycles.js';
5
5
  import { DEFAULT_WORKING_ACTIVITY, randomWorkingActivity } from '../../../shared/working-activity.js';
6
+ import { hydrateLiveAdvertisedCommandMessage, prepareAdvertisedCommandPersistence, } from '../advertised-command-invocation.js';
6
7
  const MESSAGE_UPDATE_COALESCE_MS = 75;
7
8
  /** Projects one live engine event stream to broker viewers and owns its relay timers. */
8
9
  export class EventProjection {
@@ -67,6 +68,12 @@ export class EventProjection {
67
68
  relayEvent(event, generation) {
68
69
  this.deps.faultRetry.onEvent(event);
69
70
  const type = event.type;
71
+ if (type === 'message_start') {
72
+ hydrateLiveAdvertisedCommandMessage(event.message);
73
+ }
74
+ else if (type === 'message_end') {
75
+ prepareAdvertisedCommandPersistence(this.deps.currentSession(), event.message);
76
+ }
70
77
  if (type === 'message_start' && event.message.role === 'user') {
71
78
  try {
72
79
  this.workingActivity = randomWorkingActivity(readConfig('user').working_gerunds);
@@ -1,5 +1,6 @@
1
1
  import type { AgentSessionServices, ModelRegistry, PromptOptions } from '@earendil-works/pi-coding-agent';
2
2
  import type { ImageContent } from '@earendil-works/pi-ai';
3
+ export { promptWithAdvertisedCommandInvocation } from '../advertised-command-invocation.js';
3
4
  import { type BrokerSdkConfig } from '../launch.js';
4
5
  import { type ClientToBroker } from '../broker-protocol.js';
5
6
  import type { BrokerClient, BrokerClientRegistry } from './client-registry.js';
@@ -237,4 +238,3 @@ export declare function createFrameDispatchContext(deps: FrameDispatchDeps): {
237
238
  };
238
239
  /** The socket listener calls this explicit dispatcher for every decoded client frame. */
239
240
  export declare function handleFrame(ctx: FrameDispatchContext, client: BrokerClient, frame: ClientToBroker): void;
240
- export {};
@@ -9,6 +9,9 @@ import { operationIdContext } from '../../events/operation-id.js';
9
9
  import { buildRefInventory } from '../../memory/inline-ref-inventory.js';
10
10
  import { buildGuidance } from '../../memory/inline-ref-guidance.js';
11
11
  import { clearSessionCache } from '../../substrate/session-cache.js';
12
+ import { captureSessionPromptTemplateChatSurfaces } from '../command-surface.js';
13
+ import { promptWithAdvertisedCommandInvocation } from '../advertised-command-invocation.js';
14
+ export { promptWithAdvertisedCommandInvocation } from '../advertised-command-invocation.js';
12
15
  import { configuredLadderProviders, isProviderPinnedModelToken, nextLadderModel, normalizeModel, } from '../launch.js';
13
16
  import { clearFault, readFault } from '../fault.js';
14
17
  import { isManagedProvider, readSubscriptionPool, } from '../../subscription-state.js';
@@ -451,9 +454,7 @@ export function createFrameDispatchContext(deps) {
451
454
  // escalated by m-C — so it gets a fresh operation id. A true
452
455
  // (mid-stream) followUp rides the current turn's operation, like
453
456
  // steer.
454
- return settle(trackPromptDispatch(operationIdContext.fresh(() => currentSession()
455
- .prompt(frame.text, route.options)
456
- .catch(relayError))));
457
+ return settle(trackPromptDispatch(operationIdContext.fresh(() => promptWithAdvertisedCommandInvocation(currentSession(), frame.text, () => currentSession().prompt(frame.text, route.options)).catch(relayError))));
457
458
  }
458
459
  // C1: pi's followUp() takes images as a POSITIONAL 2nd arg (`followUp(text, images?)`).
459
460
  return settle(currentSession()
@@ -470,10 +471,9 @@ export function createFrameDispatchContext(deps) {
470
471
  // ATOMICALLY clears its own pending entry if prompt() settles
471
472
  // without ever reaching that drain (an early throw/return leaves
472
473
  // nothing stranded to leak into a later, unrelated turn).
473
- return settle(trackPromptDispatch(promptWithNextTurnGuidance(currentSession(), guidance, () => operationIdContext.fresh(() => currentSession().prompt(frame.text, route.options))).catch(relayError)));
474
+ return settle(trackPromptDispatch(promptWithNextTurnGuidance(currentSession(), guidance, () => operationIdContext.fresh(() => promptWithAdvertisedCommandInvocation(currentSession(), frame.text, () => currentSession().prompt(frame.text, route.options)))).catch(relayError)));
474
475
  }
475
- return settle(trackPromptDispatch(operationIdContext.fresh(() => currentSession()
476
- .prompt(frame.text, route.options)
476
+ return settle(trackPromptDispatch(operationIdContext.fresh(() => promptWithAdvertisedCommandInvocation(currentSession(), frame.text, () => currentSession().prompt(frame.text, route.options))
477
477
  .catch(relayError)
478
478
  .then(() => sendGuidance(guidance, deliverAs)))));
479
479
  }
@@ -511,12 +511,10 @@ export function createFrameDispatchContext(deps) {
511
511
  // and executes it immediately even while streaming. It also expands a
512
512
  // matching skill/template before applying the same steer behavior.
513
513
  if (resolveSteerCall(frame.text, currentSession()) === 'prompt') {
514
- void currentSession()
515
- .prompt(frame.text, {
514
+ void promptWithAdvertisedCommandInvocation(currentSession(), frame.text, () => currentSession().prompt(frame.text, {
516
515
  images: frame.images,
517
516
  streamingBehavior: 'steer',
518
- })
519
- .catch(relayError);
517
+ })).catch(relayError);
520
518
  break;
521
519
  }
522
520
  const guidance = resolveFrameGuidance(frame.text);
@@ -1223,8 +1221,15 @@ export function createFrameDispatchContext(deps) {
1223
1221
  agent.steeringMode = 'all';
1224
1222
  agent.followUpMode = 'all';
1225
1223
  }
1224
+ // `session.reload()` has replaced pi's loaded template objects;
1225
+ // capture their crouter-only metadata before any observer refresh.
1226
+ captureSessionPromptTemplateChatSurfaces(currentSession());
1226
1227
  memoizedRefInventory = null;
1227
1228
  clearSessionCache();
1229
+ registry.forEachHelloed((observer) => {
1230
+ if (observer.role === 'observer')
1231
+ registry.sendFrame(observer, { type: 'resources_reloaded' });
1232
+ });
1228
1233
  ackTo(client, 'reload');
1229
1234
  })
1230
1235
  .catch(engineErrorTo(client));
@@ -9,6 +9,10 @@ export type CommandDisclosure = {
9
9
  source: string;
10
10
  template?: string;
11
11
  expansion?: DeterministicCommandExpansion;
12
+ /** Whether this command can complete its outcome from a non-terminal chat
13
+ * surface. Opt-in and fail-closed; builtins are never chat-capable. */
14
+ gatewayCapable: boolean;
15
+ argumentHint?: string;
12
16
  };
13
17
  export declare const modelKey: (model: {
14
18
  provider: string;
@@ -3,6 +3,7 @@ import { boundaryRootedTree } from '../session-visibility.js';
3
3
  import { isUnknownModel } from '../model-selection.js';
4
4
  import { BUILTIN_SLASH_COMMANDS, piSessionsRoot } from '../pi-vendored.js';
5
5
  import { isDeterministicCommandExpansion } from '../command-expansion.js';
6
+ import { registrationChatSurface, templateChatSurface } from '../command-surface.js';
6
7
  function registryOf(session) {
7
8
  return new ModelRegistry(session.modelRuntime);
8
9
  }
@@ -10,8 +11,10 @@ const toModelRef = (model) => model === undefined || isUnknownModel(model) ? nul
10
11
  export const modelKey = (model) => `${model.provider}/${model.id}`;
11
12
  export function buildCommandList(session) {
12
13
  const out = [];
14
+ // Builtins are pi's own terminal controls. They stay in the terminal
15
+ // disclosure and are never offered to a chat surface.
13
16
  for (const builtin of BUILTIN_SLASH_COMMANDS)
14
- out.push({ name: builtin.name, description: builtin.description, source: 'builtin' });
17
+ out.push({ name: builtin.name, description: builtin.description, source: 'builtin', gatewayCapable: false });
15
18
  try {
16
19
  for (const command of session.extensionRunner.getRegisteredCommands()) {
17
20
  const expansion = command.expansion;
@@ -20,6 +23,7 @@ export function buildCommandList(session) {
20
23
  description: command.description ?? '',
21
24
  source: 'command',
22
25
  ...(isDeterministicCommandExpansion(expansion) ? { expansion } : {}),
26
+ ...registrationChatSurface(command),
23
27
  });
24
28
  }
25
29
  }
@@ -28,7 +32,7 @@ export function buildCommandList(session) {
28
32
  }
29
33
  try {
30
34
  for (const template of session.promptTemplates)
31
- out.push({ name: template.name, description: template.description, source: 'template', template: template.content });
35
+ out.push({ name: template.name, description: template.description, source: 'template', template: template.content, ...templateChatSurface(template) });
32
36
  }
33
37
  catch {
34
38
  /* ignore */
@@ -73,7 +73,7 @@ function profileNote(node) {
73
73
  const entry = loadProfileManifest(node.profile_id);
74
74
  if (entry.manifest.projects.length === 0)
75
75
  return '';
76
- return `Your working dir is \`${node.cwd}\`. Your profile **${entry.manifest.name}** also has purview over: ${entry.manifest.projects.join(', ')}.`;
76
+ return `Your working dir is \`${node.cwd}\`. Your profile **${entry.manifest.name}** also has purview over: ${entry.manifest.projects.map((project) => project.path).join(', ')}.`;
77
77
  }
78
78
  catch {
79
79
  return '';
@@ -0,0 +1,5 @@
1
+ import type { ChatInventoryDTO, ChatInventoryMemoryRefDTO } from '../../api/dto/chat-inventory.js';
2
+ export declare function gatewayMemoryRows(refs: unknown): ChatInventoryMemoryRefDTO[];
3
+ /** Collect the chat inventories for one node. Always resolves — every failure
4
+ * mode is expressed in the returned document, not thrown. */
5
+ export declare function readChatInventory(nodeId: string): Promise<ChatInventoryDTO>;
@@ -0,0 +1,191 @@
1
+ // broker-inventory.ts — one short-lived OBSERVER round trip against a live
2
+ // node's broker that collects both chat-surface inventories at once: the
3
+ // command disclosure (`get_commands`) and the inline memory-reference
4
+ // inventory (`list_memory_refs`).
5
+ //
6
+ // It is modelled on `broker-request.ts`'s `oneShotControllerRequest` — the
7
+ // sanctioned host→live-broker dance — but is a different shape: two frames on
8
+ // one connection, settling on two independent answers, and never writable.
9
+ // Both read-ops are explicitly observer-safe, so this connection never claims
10
+ // controller and can never drive the engine.
11
+ //
12
+ // The two answers arrive by different routes because the frames differ.
13
+ // `get_commands` is uncorrelated and answers with an `ack`, which surfaces on
14
+ // the client's `frame` event. `list_memory_refs` answers with a correlated
15
+ // `data` frame, and `BrokerClient` swallows EVERY `data` frame into its
16
+ // pending-by-id resolver rather than re-emitting it — so that half must ride
17
+ // `client.request()`, which mints the id and owns the reply.
18
+ //
19
+ // A read NEVER revives. A dormant node, a refused connection, or an expired
20
+ // budget answers `broker_live:false` with empty arrays; one answer arriving
21
+ // without the other yields that part plus an empty peer. Nothing here is fatal,
22
+ // because a chat surface's behavior for "empty because it failed" and "empty
23
+ // because it is empty" is identical.
24
+ import { randomUUID } from 'node:crypto';
25
+ import { getNode } from '../canvas/canvas.js';
26
+ import { BrokerClient } from '../broker-client/index.js';
27
+ import { isDeterministicCommandExpansion } from './command-expansion.js';
28
+ import { isBrokerLive } from './model-swap.js';
29
+ /** Same budget as `broker-request.ts`: this is local engine work, so it is
30
+ * generous slack rather than a tuned deadline. One budget covers BOTH answers —
31
+ * whichever part has not arrived when it expires is returned empty. */
32
+ const READ_TIMEOUT_MS = 15_000;
33
+ /** The transcript window this reader asks for in its welcome. Any hello triggers
34
+ * `sendWelcome`, and an untailed welcome makes the broker build and encode the
35
+ * whole conversation on its serialized frame loop for a payload this reader
36
+ * throws away — on every activation and reconnect, worst on the longest
37
+ * conversations. One entry is the smallest legal request. */
38
+ const SNAPSHOT_TAIL = 1;
39
+ const MEMORY_KINDS = new Set(['knowledge', 'preference']);
40
+ const MEMORY_SCOPES = new Set(['node', 'project', 'profile', 'user', 'builtin']);
41
+ function nonEmptyString(value) {
42
+ return typeof value === 'string' && value !== '';
43
+ }
44
+ /** Keep the rows a chat surface can actually use and drop the rest. Malformed
45
+ * rows are discarded individually so one bad registration cannot take down the
46
+ * whole inventory.
47
+ *
48
+ * This validation lives here rather than reusing the attach viewer's parser:
49
+ * `src/core` does not import `src/clients`, and adding that import would invert
50
+ * the layering. */
51
+ function commandRows(detail) {
52
+ if (detail === undefined)
53
+ return [];
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(detail);
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ if (!Array.isArray(parsed))
62
+ return [];
63
+ const out = [];
64
+ for (const row of parsed) {
65
+ if (typeof row !== 'object' || row === null)
66
+ continue;
67
+ const record = row;
68
+ if (record['gatewayCapable'] !== true)
69
+ continue;
70
+ const source = record['source'];
71
+ if (source !== 'command' && source !== 'template')
72
+ continue;
73
+ if (!nonEmptyString(record['name']) || typeof record['description'] !== 'string')
74
+ continue;
75
+ const expansion = record['expansion'];
76
+ const template = record['template'];
77
+ const argumentHint = record['argumentHint'];
78
+ out.push({
79
+ name: record['name'],
80
+ description: record['description'],
81
+ source,
82
+ ...(nonEmptyString(argumentHint) ? { argument_hint: argumentHint } : {}),
83
+ ...(isDeterministicCommandExpansion(expansion) ? { expansion } : {}),
84
+ ...(typeof template === 'string' ? { template } : {}),
85
+ });
86
+ }
87
+ return out;
88
+ }
89
+ export function gatewayMemoryRows(refs) {
90
+ if (!Array.isArray(refs))
91
+ return [];
92
+ const out = [];
93
+ for (const row of refs) {
94
+ if (typeof row !== 'object' || row === null)
95
+ continue;
96
+ const record = row;
97
+ if (record['gatewayVisible'] !== true)
98
+ continue;
99
+ if (!nonEmptyString(record['name']) || typeof record['shortForm'] !== 'string')
100
+ continue;
101
+ if (typeof record['kind'] !== 'string' || !MEMORY_KINDS.has(record['kind']))
102
+ continue;
103
+ if (typeof record['scope'] !== 'string' || !MEMORY_SCOPES.has(record['scope']))
104
+ continue;
105
+ out.push({
106
+ name: record['name'],
107
+ kind: record['kind'],
108
+ scope: record['scope'],
109
+ shortForm: record['shortForm'],
110
+ });
111
+ }
112
+ return out;
113
+ }
114
+ const empty = (nodeId) => ({
115
+ node_id: nodeId,
116
+ broker_live: false,
117
+ commands: [],
118
+ memory_refs: [],
119
+ });
120
+ /** Collect the chat inventories for one node. Always resolves — every failure
121
+ * mode is expressed in the returned document, not thrown. */
122
+ export function readChatInventory(nodeId) {
123
+ const meta = getNode(nodeId);
124
+ if (meta === null || !isBrokerLive(meta))
125
+ return Promise.resolve(empty(nodeId));
126
+ return new Promise((resolve) => {
127
+ const client = new BrokerClient(nodeId);
128
+ const clientId = randomUUID();
129
+ let settled = false;
130
+ let connected = false;
131
+ let commands;
132
+ let memoryRefs;
133
+ const timer = setTimeout(() => finish(), READ_TIMEOUT_MS);
134
+ if (typeof timer.unref === 'function')
135
+ timer.unref();
136
+ /** Settle once with whatever arrived. A part that never answered stays
137
+ * empty; a connection that never established reports a dormant broker. */
138
+ function finish() {
139
+ if (settled)
140
+ return;
141
+ settled = true;
142
+ clearTimeout(timer);
143
+ try {
144
+ client.close();
145
+ }
146
+ catch {
147
+ /* best-effort */
148
+ }
149
+ resolve({
150
+ node_id: nodeId,
151
+ broker_live: connected,
152
+ commands: commands ?? [],
153
+ memory_refs: memoryRefs ?? [],
154
+ });
155
+ }
156
+ function settleWhenComplete() {
157
+ if (commands !== undefined && memoryRefs !== undefined)
158
+ finish();
159
+ }
160
+ client.on('error', () => finish());
161
+ client.on('close', () => finish());
162
+ client.on('connect', () => {
163
+ connected = true;
164
+ client.send({ type: 'hello', role: 'observer', client_id: clientId, snapshot_tail: SNAPSHOT_TAIL });
165
+ client.send({ type: 'get_commands' });
166
+ client.request({ type: 'list_memory_refs' }).then((frame) => {
167
+ memoryRefs = frame.kind === 'list_memory_refs' ? gatewayMemoryRows(frame.refs) : [];
168
+ settleWhenComplete();
169
+ }, () => {
170
+ memoryRefs = [];
171
+ settleWhenComplete();
172
+ });
173
+ });
174
+ client.on('frame', (frame) => {
175
+ // `get_commands` carries no id, so its ack is matched on `for` alone.
176
+ if (frame.type === 'ack' && frame.for === 'get_commands') {
177
+ commands = frame.ok ? commandRows(frame.detail) : [];
178
+ settleWhenComplete();
179
+ return;
180
+ }
181
+ // The memory half's correlated error is consumed by its own request
182
+ // rejection, so an error reaching here is uncorrelated and can only be
183
+ // attributed to the uncorrelated half.
184
+ if (frame.type === 'error' && frame.id === undefined) {
185
+ commands ??= [];
186
+ settleWhenComplete();
187
+ }
188
+ });
189
+ client.connect();
190
+ });
191
+ }