@xfey/tutti 0.1.107 → 0.1.109

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 (72) hide show
  1. package/README.md +3 -0
  2. package/dist/chat-assistant/diagnostics.d.ts +17 -0
  3. package/dist/chat-assistant/diagnostics.js +27 -0
  4. package/dist/chat-assistant/index.d.ts +4 -0
  5. package/dist/chat-assistant/index.js +30 -7
  6. package/dist/collaboration-ingestion/external-source.js +10 -1
  7. package/dist/control-plane/task-source-context-types.d.ts +2 -1
  8. package/dist/control-plane/task-source-context.js +7 -0
  9. package/dist/providers/invocation-progress.d.ts +10 -0
  10. package/dist/providers/invocation-progress.js +9 -0
  11. package/dist/providers/openai/app-server/read-only-procedure.d.ts +2 -0
  12. package/dist/providers/openai/app-server/read-only-procedure.js +34 -2
  13. package/dist/providers/openai/app-server/skill-discovery.d.ts +10 -0
  14. package/dist/providers/openai/app-server/skill-discovery.js +29 -0
  15. package/dist/providers/openai/app-server/skills.d.ts +8 -17
  16. package/dist/providers/openai/app-server/skills.js +16 -31
  17. package/dist/providers/openai/app-server/workspace-write-run.js +24 -26
  18. package/dist/providers/openai/chat-assistant.js +12 -2
  19. package/dist/reference-access/cache.d.ts +50 -0
  20. package/dist/reference-access/cache.js +268 -0
  21. package/dist/reference-access/read-evidence.d.ts +6 -0
  22. package/dist/reference-access/read-evidence.js +15 -0
  23. package/dist/reference-access/service.d.ts +26 -0
  24. package/dist/reference-access/service.js +42 -0
  25. package/dist/server-shell/cli/external-reference-runtime.d.ts +12 -0
  26. package/dist/server-shell/cli/external-reference-runtime.js +75 -0
  27. package/dist/server-shell/cli/host-server-runtime.js +19 -1
  28. package/dist/server-shell/cli/relay-registration.js +1 -0
  29. package/dist/server-shell/http/create-server.d.ts +2 -0
  30. package/dist/server-shell/http/create-server.js +5 -4
  31. package/dist/server-shell/http/routes/agent-context-auth.d.ts +2 -1
  32. package/dist/server-shell/http/routes/agent-context-auth.js +3 -1
  33. package/dist/server-shell/http/routes/agent-context-external-reference-routes.d.ts +4 -0
  34. package/dist/server-shell/http/routes/agent-context-external-reference-routes.js +108 -0
  35. package/dist/server-shell/http/routes/agent-context-shared.d.ts +2 -0
  36. package/dist/server-shell/http/routes/agent-context.js +3 -1
  37. package/dist/server-shell/http/static-web.js +10 -6
  38. package/dist/workspace-ops/project-workspace.js +6 -6
  39. package/dist/workspace-ops/reference-cache-policy.d.ts +6 -0
  40. package/dist/workspace-ops/reference-cache-policy.js +79 -0
  41. package/migrations/0021_external_reference_reads.sql +11 -0
  42. package/node_modules/@tutti/relay-client/dist/external-references.d.ts +11 -0
  43. package/node_modules/@tutti/relay-client/dist/external-references.js +63 -0
  44. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +1 -0
  45. package/node_modules/@tutti/relay-client/dist/host-control.js +1 -0
  46. package/node_modules/@tutti/relay-client/dist/index.d.ts +1 -0
  47. package/node_modules/@tutti/relay-client/dist/index.js +1 -0
  48. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +6 -0
  49. package/node_modules/@tutti/shared/dist/schemas/api/external-references.d.ts +23 -0
  50. package/node_modules/@tutti/shared/dist/schemas/api/external-references.js +21 -0
  51. package/node_modules/@tutti/shared/dist/schemas/api/feishu-documents.d.ts +6 -1
  52. package/node_modules/@tutti/shared/dist/schemas/api/feishu-documents.js +16 -3
  53. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +3 -2
  54. package/node_modules/@tutti/shared/dist/schemas/api/index.js +2 -1
  55. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +24 -0
  56. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +2 -0
  57. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +7 -1
  58. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +17 -3
  59. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +6 -0
  60. package/node_modules/@tutti/shared/dist/schemas/internal/external-references.d.ts +109 -0
  61. package/node_modules/@tutti/shared/dist/schemas/internal/external-references.js +70 -0
  62. package/node_modules/@tutti/shared/dist/schemas/internal/feishu-documents.d.ts +11 -0
  63. package/node_modules/@tutti/shared/dist/schemas/internal/feishu-documents.js +5 -0
  64. package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +1 -0
  65. package/node_modules/@tutti/shared/dist/schemas/internal/index.js +1 -0
  66. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +36 -0
  67. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +3 -0
  68. package/package.json +1 -1
  69. package/prompts/skills/read-references/SKILL.md +32 -3
  70. package/web/assets/{homepage-motion-scene-CFs5rMWq.js → homepage-motion-scene-B6yPJnY8.js} +1 -1
  71. package/web/assets/{index-BmiLZmaV.js → index-aQnJ5JoI.js} +3 -3
  72. package/web/index.html +1 -1
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  `apps/server` 是 Tutti 的项目 host service,也是机器级 Local Console 的进程入口。
6
6
 
7
+ `0.1.109` 的 Codex skill 接入统一验证 `skills/extraRoots/set` / `skills/list` 与实际 `SKILL.md` 文件身份;Chat 的安全阶段日志可通过 invocation ref 关联外部 Reference 读取。版本发布、测试 Host 切换和飞书真人门禁分别记录于[按需资料计划](../../docs/roadmap/feishu-on-demand-reference-plan.md),不以安装版本代替实际读取证据。
8
+
7
9
  它负责:
8
10
 
9
11
  - `tutti launch` 后的项目绑定、恢复和接管入口
@@ -15,6 +17,7 @@
15
17
  - host 到 Relay 的连接与工作区加入协作
16
18
  - machine-local `standalone | feishu` Project Surface authority,以及Standalone browser session / Feishu trusted integration的双向Host gate
17
19
  - Feishu主群聊Chat Assistant canonical response与Host-owned durable outbound handoff intent
20
+ - Feishu 外部来源的按需读取、受控临时 cache、invocation 版本和 SQLite 读取证据;正式 Reference/Standalone 上传与历史 snapshot 保持,见 [external-reference-access.md](../../docs/system/external-reference-access.md)
18
21
 
19
22
  ## 结构
20
23
 
@@ -0,0 +1,17 @@
1
+ import type { ProviderInvocationProgress } from "../providers/invocation-progress.js";
2
+ export type ChatAssistantLogger = {
3
+ info: (fields: Record<string, unknown>, message: string) => void;
4
+ warn: (fields: Record<string, unknown>, message: string) => void;
5
+ };
6
+ type ChatAssistantProgress = ProviderInvocationProgress | {
7
+ stage: "queued" | "inspection_wait" | "model_requested" | "reply_persisted" | "failed" | "skipped";
8
+ reason_code?: "queue_full" | "stopping" | "context_unavailable" | "answer_failed" | "reply_failed";
9
+ };
10
+ export declare function createChatAssistantDiagnostics(options: {
11
+ logger?: ChatAssistantLogger;
12
+ projectId: string;
13
+ triggerMessageId: string;
14
+ scope: string;
15
+ }): (event: ChatAssistantProgress) => void;
16
+ export {};
17
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { createEventRef } from "@tutti/shared/ids";
2
+ export function createChatAssistantDiagnostics(options) {
3
+ const requestRef = createEventRef();
4
+ const started = performance.now();
5
+ return (event) => {
6
+ try {
7
+ const fields = {
8
+ project_id: options.projectId,
9
+ chat_request_ref: requestRef,
10
+ trigger_message_ref: options.triggerMessageId,
11
+ scope: options.scope,
12
+ elapsed_ms: Math.round(performance.now() - started),
13
+ ...event,
14
+ };
15
+ if (event.stage === "failed" || event.stage === "provider_failed") {
16
+ options.logger?.warn(fields, "Chat assistant progress");
17
+ }
18
+ else {
19
+ options.logger?.info(fields, "Chat assistant progress");
20
+ }
21
+ }
22
+ catch {
23
+ /* Logging must not replace the answer or interrupt queue draining. */
24
+ }
25
+ };
26
+ }
27
+ //# sourceMappingURL=diagnostics.js.map
@@ -1,5 +1,7 @@
1
1
  import type { ProjectId } from "@tutti/shared/ids";
2
2
  import type { MessageProjection } from "@tutti/shared/schemas/api";
3
+ import { type ChatAssistantLogger } from "./diagnostics.js";
4
+ import type { ProviderInvocationProgressObserver } from "../providers/invocation-progress.js";
3
5
  import { type HostProjectStore, type SqliteDatabase } from "../store/index.js";
4
6
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
5
7
  export declare const CHAT_ASSISTANT_OUTPUT_SCHEMA: {
@@ -19,6 +21,7 @@ export type ChatAssistantOutput = {
19
21
  };
20
22
  export type ChatAssistantPromptInput = {
21
23
  prompt: string;
24
+ onProgress?: ProviderInvocationProgressObserver;
22
25
  };
23
26
  export type ReadOnlyChatAssistantModel = {
24
27
  answer: (input: ChatAssistantPromptInput) => Promise<ChatAssistantOutput>;
@@ -35,6 +38,7 @@ export type ReadOnlyChatAssistantHandle = {
35
38
  stopAccepting: () => void;
36
39
  };
37
40
  export type ReadOnlyChatAssistantOptions = {
41
+ logger?: ChatAssistantLogger;
38
42
  project: {
39
43
  project_id: ProjectId;
40
44
  display_name: string;
@@ -1,3 +1,4 @@
1
+ import { createChatAssistantDiagnostics } from "./diagnostics.js";
1
2
  import { createClarificationRoundAgentMessage, createMainChatAgentMessage, readClarificationRoundMessagesWindow, readClarificationRoundProjection, readMainChatMessageWindow, readMainChatMessagesBeforeCursor, readMessageProjectionById, } from "../collaboration-state/index.js";
2
3
  import { renderPromptTemplate } from "../prompt-templates/index.js";
3
4
  import { withHostStoreTransaction, } from "../store/index.js";
@@ -111,13 +112,21 @@ export class ReadOnlyChatAssistant {
111
112
  if (!this.accepting) {
112
113
  return { triggered: false };
113
114
  }
115
+ const report = createChatAssistantDiagnostics({
116
+ ...(this.options.logger === undefined ? {} : { logger: this.options.logger }),
117
+ projectId: this.options.project.project_id,
118
+ triggerMessageId: input.message.id,
119
+ scope: input.message.scope.kind,
120
+ });
114
121
  if (this.pending.length >= this.maxQueuedMentions) {
122
+ report({ stage: "failed", reason_code: "queue_full" });
115
123
  this.appendAssistantMessageForTrigger(input.message, assistantFailureMessage(), {
116
124
  scratchpad_source: "exclude",
117
125
  });
118
126
  return { triggered: true };
119
127
  }
120
- this.pending.push(input.message);
128
+ report({ stage: "queued" });
129
+ this.pending.push({ message: input.message, report });
121
130
  this.pump();
122
131
  return { triggered: true };
123
132
  }
@@ -132,7 +141,8 @@ export class ReadOnlyChatAssistant {
132
141
  return;
133
142
  }
134
143
  this.accepting = false;
135
- for (const message of this.pending.splice(0)) {
144
+ for (const { message, report } of this.pending.splice(0)) {
145
+ report({ stage: "skipped", reason_code: "stopping" });
136
146
  this.appendAssistantMessageForTrigger(message, assistantFailureMessage(), {
137
147
  scratchpad_source: "exclude",
138
148
  });
@@ -143,12 +153,14 @@ export class ReadOnlyChatAssistant {
143
153
  if (this.active !== null) {
144
154
  return;
145
155
  }
146
- const message = this.pending.shift();
147
- if (message === undefined) {
156
+ const pending = this.pending.shift();
157
+ if (pending === undefined) {
148
158
  this.resolveIdleIfNeeded();
149
159
  return;
150
160
  }
151
- const task = this.respond(message).catch(() => undefined);
161
+ const task = this.respond(pending.message, pending.report).catch(() => {
162
+ pending.report({ stage: "failed", reason_code: "reply_failed" });
163
+ });
152
164
  this.active = task;
153
165
  void task.finally(() => {
154
166
  if (this.active === task) {
@@ -311,23 +323,34 @@ export class ReadOnlyChatAssistant {
311
323
  }
312
324
  this.appendMainChatAssistantMessage(triggerMessage, body, refs);
313
325
  }
314
- async respond(triggerMessage) {
326
+ async respond(triggerMessage, report) {
327
+ let answer;
315
328
  try {
329
+ report({ stage: "inspection_wait" });
316
330
  await this.options.waitForInspectionAvailability?.();
317
331
  const prompt = this.renderPrompt(triggerMessage);
318
332
  if (prompt === null) {
333
+ report({ stage: "skipped", reason_code: "context_unavailable" });
319
334
  return;
320
335
  }
336
+ report({ stage: "model_requested" });
321
337
  const output = await this.options.model.answer({
322
338
  prompt,
339
+ onProgress: report,
323
340
  });
324
- this.appendAssistantMessageForTrigger(triggerMessage, normalizeAnswer(output.answer));
341
+ answer = normalizeAnswer(output.answer);
325
342
  }
326
343
  catch {
344
+ report({ stage: "failed", reason_code: "answer_failed" });
327
345
  this.appendAssistantMessageForTrigger(triggerMessage, assistantFailureMessage(), {
328
346
  scratchpad_source: "exclude",
329
347
  });
348
+ report({ stage: "reply_persisted" });
349
+ return;
330
350
  }
351
+ // Persistence/outbound-hook failure must not retry by creating a second reply.
352
+ this.appendAssistantMessageForTrigger(triggerMessage, answer);
353
+ report({ stage: "reply_persisted" });
331
354
  }
332
355
  }
333
356
  //# sourceMappingURL=index.js.map
@@ -86,7 +86,13 @@ export function ingestTrustedConversationSource(options) {
86
86
  account_ref: options.command.actor.account_ref,
87
87
  display_name: options.command.actor.display_name,
88
88
  },
89
- materialize: () => ({ kind: "message", body: source.content.text }),
89
+ materialize: () => ({
90
+ kind: "message",
91
+ body: source.content.text,
92
+ ...(source.external_references === undefined
93
+ ? {}
94
+ : { refs: { external_references: source.external_references } }),
95
+ }),
90
96
  idempotencyContext: {
91
97
  kind: "external_conversation_source",
92
98
  platform: source.locator.conversation.installation.platform,
@@ -94,6 +100,9 @@ export function ingestTrustedConversationSource(options) {
94
100
  external_message_ref: source.locator.message_ref,
95
101
  binding_ref: options.command.binding_ref,
96
102
  receipt_key: options.command.receipt.receipt_key,
103
+ ...(source.external_references === undefined
104
+ ? {}
105
+ : { external_references: source.external_references }),
97
106
  },
98
107
  afterCreate: (tx, message) => insertExternalSourceEvidence(tx, options.command, message),
99
108
  ...(options.scratchpadSource === undefined
@@ -1,8 +1,9 @@
1
- import type { ClarificationRequestPayload } from "@tutti/shared/schemas/api";
1
+ import type { ClarificationRequestPayload, ExternalReferenceDescriptor } from "@tutti/shared/schemas/api";
2
2
  export type PromptSourceMessage = {
3
3
  role: "human" | "tutti";
4
4
  author_display_name?: string;
5
5
  text: string;
6
+ external_references?: ExternalReferenceDescriptor[];
6
7
  };
7
8
  export type TaskCompileClarificationContext = {
8
9
  request_payload: ClarificationRequestPayload;
@@ -16,6 +16,13 @@ function promptSourceMessage(db, source) {
16
16
  ? {}
17
17
  : { author_display_name: displayName }),
18
18
  text: messageTextForModel(message),
19
+ ...(message.refs?.external_references === undefined
20
+ ? {}
21
+ : {
22
+ external_references: message.refs.external_references.map((reference) => ({
23
+ ...reference,
24
+ })),
25
+ }),
19
26
  };
20
27
  }
21
28
  if (message.message_kind === "agent_text" && message.author.kind === "agent") {
@@ -0,0 +1,10 @@
1
+ export type ProviderInvocationProgress = {
2
+ stage: "provider_queued" | "provider_started" | "context_ready" | "skills_ready" | "turn_started" | "turn_completed" | "provider_failed";
3
+ invocation_ref?: string;
4
+ skill_count?: number;
5
+ completed_tool_count?: number;
6
+ reason_code?: "provider_unavailable" | "app_server_protocol_error" | "auth_failed" | "thread_start_failed" | "turn_failed" | "turn_timeout" | "unexpected_server_request" | "skill_selection_failed" | "output_parse_failed" | "output_validation_failed" | "file_read_not_observed" | "write_detected" | "secret_leak_detected" | "process_lifecycle_failed" | "admission_unavailable";
7
+ };
8
+ export type ProviderInvocationProgressObserver = (event: ProviderInvocationProgress) => void;
9
+ export declare function reportProviderInvocationProgress(observer: ProviderInvocationProgressObserver | undefined, event: ProviderInvocationProgress): void;
10
+ //# sourceMappingURL=invocation-progress.d.ts.map
@@ -0,0 +1,9 @@
1
+ export function reportProviderInvocationProgress(observer, event) {
2
+ try {
3
+ observer?.(event);
4
+ }
5
+ catch {
6
+ /* Diagnostics cannot change the provider outcome. */
7
+ }
8
+ }
9
+ //# sourceMappingURL=invocation-progress.js.map
@@ -1,3 +1,4 @@
1
+ import { type ProviderInvocationProgressObserver } from "../../invocation-progress.js";
1
2
  import { type CodexAppServerLifecycleIncident } from "./invocation-lifecycle.js";
2
3
  import { type CodexAppServerInvocationCoordinator, type CodexAppServerInvocationLease } from "./invocation-coordinator.js";
3
4
  import { type CodexAppServerRuntimeTelemetryOptions } from "./runtime-telemetry.js";
@@ -5,6 +6,7 @@ import { type CodexAppServerAgentContextRuntime, type CodexAppServerSkillSelecti
5
6
  import { type OpenAiTokenUsage } from "../token-usage.js";
6
7
  export type CodexAppServerWebSearchMode = "disabled" | "cached" | "live";
7
8
  export type CodexAppServerReadOnlyProcedureInput<TOutput> = {
9
+ onProgress?: ProviderInvocationProgressObserver;
8
10
  apiKey: string;
9
11
  apiBaseUrl?: string;
10
12
  defaultModel?: string;
@@ -1,7 +1,9 @@
1
1
  import { execFileSync, spawn } from "node:child_process";
2
2
  import { mkdirSync, mkdtempSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
+ import { discoverCodexAppServerSkills } from "./skill-discovery.js";
6
+ import { reportProviderInvocationProgress, } from "../../invocation-progress.js";
5
7
  import { buildCodexAppServerProcessPlan } from "../codex-app-server.js";
6
8
  import { DEFAULT_OPENAI_MODEL } from "../model-config.js";
7
9
  import { CodexAppServerJsonRpcClient, CodexAppServerProtocolError } from "./json-rpc.js";
@@ -113,6 +115,7 @@ function structuredOutputDetails(client, text) {
113
115
  }
114
116
  export async function runCodexAppServerReadOnlyProcedure(options) {
115
117
  if (options.invocationCoordinator !== undefined && options.invocationLease === undefined) {
118
+ reportProviderInvocationProgress(options.onProgress, { stage: "provider_queued" });
116
119
  let lease;
117
120
  try {
118
121
  lease = await options.invocationCoordinator.acquire();
@@ -132,6 +135,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
132
135
  lease.release();
133
136
  }
134
137
  }
138
+ reportProviderInvocationProgress(options.onProgress, { stage: "provider_started" });
135
139
  let skillInputs;
136
140
  let contextRuntimeHint;
137
141
  try {
@@ -145,6 +149,9 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
145
149
  : { builtInSkillsRoot: options.builtInSkillsRoot }),
146
150
  });
147
151
  contextRuntimeHint = resolveCodexAppServerContextRuntimeHint({
152
+ ...(options.runtimeTelemetry?.activityRef === undefined
153
+ ? {}
154
+ : { activityRef: options.runtimeTelemetry.activityRef }),
148
155
  ...(options.skillSelectionName === undefined
149
156
  ? {}
150
157
  : { selectionName: options.skillSelectionName }),
@@ -164,6 +171,13 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
164
171
  options.builtInSkillsRoot ?? "",
165
172
  ]));
166
173
  }
174
+ if (contextRuntimeHint !== undefined)
175
+ reportProviderInvocationProgress(options.onProgress, {
176
+ stage: "context_ready",
177
+ ...(contextRuntimeHint.invocation_ref === undefined
178
+ ? {}
179
+ : { invocation_ref: contextRuntimeHint.invocation_ref }),
180
+ });
167
181
  const model = options.defaultModel ?? DEFAULT_OPENAI_MODEL;
168
182
  const tempRoot = mkdtempSync(join(tmpdir(), "tutti-codex-readonly-procedure-"));
169
183
  const codexHome = options.session?.codexHome ?? join(tempRoot, "codex-home");
@@ -220,7 +234,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
220
234
  options.userSkillsRoot ?? "",
221
235
  options.builtInSkillsRoot ?? "",
222
236
  ...(contextRuntimeHint?.sensitiveValues ?? []),
223
- ...skillInputs.map((skill) => skill.path),
237
+ ...skillInputs.flatMap((skill) => [skill.path, dirname(skill.path)]),
224
238
  ];
225
239
  const strictLeakCheckValues = [options.apiKey, ...(contextRuntimeHint?.sensitiveValues ?? [])];
226
240
  let primaryError;
@@ -245,6 +259,16 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
245
259
  if (!isApiKeyAccount(loginResponse)) {
246
260
  throw new CodexAppServerReadOnlyProcedureError("auth_failed", "Codex app-server did not accept API key auth.");
247
261
  }
262
+ await discoverCodexAppServerSkills({
263
+ client,
264
+ cwd: appServerCwd,
265
+ skills: skillInputs,
266
+ requestTimeoutMs,
267
+ });
268
+ reportProviderInvocationProgress(options.onProgress, {
269
+ stage: "skills_ready",
270
+ skill_count: skillInputs.length,
271
+ });
248
272
  let threadId;
249
273
  if (options.session?.threadId === undefined) {
250
274
  const threadStartResponse = await client.request("thread/start", {
@@ -295,6 +319,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
295
319
  modelProvider,
296
320
  outputSchema: options.outputSchema,
297
321
  }, requestTimeoutMs);
322
+ reportProviderInvocationProgress(options.onProgress, { stage: "turn_started" });
298
323
  const turnCompletedPromise = client.waitForNotification((notification) => notification.method === "turn/completed", turnTimeoutMs);
299
324
  const serverRequestPromise = client.waitForServerRequest(turnTimeoutMs);
300
325
  let completedOrRequest;
@@ -347,6 +372,10 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
347
372
  const agentText = client.notifications
348
373
  .map((notification) => extractAgentText(notification))
349
374
  .join("");
375
+ reportProviderInvocationProgress(options.onProgress, {
376
+ stage: "turn_completed",
377
+ completed_tool_count: structuredOutputDetails(client, agentText).completed_tool_count ?? 0,
378
+ });
350
379
  let parsed;
351
380
  try {
352
381
  parsed = parseCodexAppServerStructuredJson(agentText);
@@ -405,6 +434,9 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
405
434
  if (error instanceof CodexAppServerReadOnlyProcedureError) {
406
435
  primaryError = error;
407
436
  }
437
+ else if (error instanceof CodexAppServerSkillSelectionError) {
438
+ primaryError = new CodexAppServerReadOnlyProcedureError("skill_selection_failed", error.message);
439
+ }
408
440
  else if (error instanceof CodexAppServerProtocolError) {
409
441
  primaryError = new CodexAppServerReadOnlyProcedureError("app_server_protocol_error", redactWithKnownValues(error.message, sensitiveValues));
410
442
  }
@@ -0,0 +1,10 @@
1
+ import { type CodexAppServerSkillInput } from "./skills.js";
2
+ export declare function discoverCodexAppServerSkills(options: {
3
+ client: {
4
+ request: (method: string, params: unknown, timeoutMs: number) => Promise<unknown>;
5
+ };
6
+ cwd: string;
7
+ skills: CodexAppServerSkillInput[];
8
+ requestTimeoutMs: number;
9
+ }): Promise<void>;
10
+ //# sourceMappingURL=skill-discovery.d.ts.map
@@ -0,0 +1,29 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { isRecord } from "./runtime-helpers.js";
4
+ import { CodexAppServerSkillSelectionError } from "./skills.js";
5
+ // Use the pinned binary's protocol, not fields accepted only by a newer client.
6
+ // Extra roots are process-local; each invocation owns its app-server process.
7
+ export async function discoverCodexAppServerSkills(options) {
8
+ if (options.skills.length === 0)
9
+ return;
10
+ try {
11
+ const cwd = realpathSync(options.cwd);
12
+ await options.client.request("skills/extraRoots/set", { extraRoots: [...new Set(options.skills.map((skill) => dirname(skill.path)))] }, options.requestTimeoutMs);
13
+ const response = await options.client.request("skills/list", { cwds: [cwd], forceReload: true }, options.requestTimeoutMs);
14
+ const entry = isRecord(response) && Array.isArray(response.data)
15
+ ? response.data.find((entry) => isRecord(entry) && entry.cwd === cwd)
16
+ : undefined;
17
+ const discovered = isRecord(entry) && Array.isArray(entry.skills) ? entry.skills : [];
18
+ if (!options.skills.every((skill) => discovered.some((candidate) => isRecord(candidate) &&
19
+ candidate.name === skill.name &&
20
+ candidate.path === skill.path &&
21
+ candidate.enabled === true)))
22
+ throw new Error("Selected skill was not discovered");
23
+ }
24
+ catch {
25
+ // No discovered paths, parser errors or arbitrary RPC error bodies in diagnostics.
26
+ throw new CodexAppServerSkillSelectionError("skill_discovery_failed", "Codex app-server could not discover the selected skills.");
27
+ }
28
+ }
29
+ //# sourceMappingURL=skill-discovery.js.map
@@ -1,3 +1,4 @@
1
+ import type { ActivityRef } from "@tutti/shared/ids";
1
2
  export type CodexAppServerSkillInput = {
2
3
  name: string;
3
4
  path: string;
@@ -15,40 +16,35 @@ export type CodexAppServerSkillInputItem = {
15
16
  export type CodexAppServerTurnInputItem = CodexAppServerSkillInputItem | CodexAppServerTextInputItem;
16
17
  export type CodexAppServerBuiltInSkillName = "read-main-chat" | "read-project-docs" | "read-scratchpad" | "read-worklist" | "read-references" | "read-user-skills";
17
18
  export type CodexAppServerSkillSelectionName = "chat_assistant" | "project_context_bootstrap" | "task_compile" | "context_sync" | "reference_summary_refresh" | "workspace_write_run" | "follow_up_run";
18
- export type CodexAppServerContextApiScope = "messages:read" | "project-docs:list" | "project-docs:read" | "scratchpad:read" | "worklist:read" | "task:read" | "references:list" | "references:read" | "skills:list" | "skills:read";
19
+ export type CodexAppServerContextApiScope = "messages:read" | "project-docs:list" | "project-docs:read" | "scratchpad:read" | "worklist:read" | "task:read" | "references:list" | "references:read" | "references:external" | "skills:list" | "skills:read";
19
20
  export type CodexAppServerSkillSelection = {
20
21
  builtInSkillNames: CodexAppServerBuiltInSkillName[];
21
22
  userSkillMode: "disabled" | "all_enabled";
22
23
  contextApiScopes: CodexAppServerContextApiScope[];
23
24
  };
24
25
  export type CodexAppServerAgentContextTokenRequest = {
26
+ activityRef?: ActivityRef;
25
27
  selectionName: CodexAppServerSkillSelectionName;
26
28
  scopes: CodexAppServerContextApiScope[];
27
29
  };
28
30
  export type CodexAppServerAgentContextToken = {
29
31
  token: string;
30
32
  expires_at?: string;
33
+ invocation_ref?: string;
31
34
  };
32
35
  export type CodexAppServerAgentContextRuntime = {
33
36
  baseUrl: string;
34
37
  issueToken: (input: CodexAppServerAgentContextTokenRequest) => CodexAppServerAgentContextToken;
35
38
  };
36
- export type CodexAppServerSkillsListWarmup = {
37
- cwds: string[];
38
- forceReload: true;
39
- perCwdExtraUserRoots: Array<{
40
- cwd: string;
41
- extraUserRoots: string[];
42
- }>;
43
- };
44
39
  export type CodexAppServerContextRuntimeHint = {
45
40
  text: string;
46
41
  sensitiveValues: string[];
47
42
  scopes: CodexAppServerContextApiScope[];
43
+ invocation_ref?: string;
48
44
  };
49
45
  export declare class CodexAppServerSkillSelectionError extends Error {
50
- readonly code: "built_in_skill_not_found" | "built_in_skill_invalid";
51
- constructor(code: "built_in_skill_not_found" | "built_in_skill_invalid", message: string);
46
+ readonly code: "built_in_skill_not_found" | "built_in_skill_invalid" | "skill_discovery_failed";
47
+ constructor(code: "built_in_skill_not_found" | "built_in_skill_invalid" | "skill_discovery_failed", message: string);
52
48
  }
53
49
  export declare const CODEX_APP_SERVER_SKILL_SELECTIONS: Record<CodexAppServerSkillSelectionName, CodexAppServerSkillSelection>;
54
50
  export declare function resolveCodexAppServerSkillSelection(selectionName: CodexAppServerSkillSelectionName): CodexAppServerSkillSelection;
@@ -58,12 +54,6 @@ export declare function resolveCodexAppServerSkillInputs(options: {
58
54
  builtInSkillsRoot?: string;
59
55
  userSkillsRoot?: string;
60
56
  }): CodexAppServerSkillInput[];
61
- export declare function resolveCodexAppServerSkillsListWarmup(options: {
62
- cwd: string;
63
- selectionName?: CodexAppServerSkillSelectionName;
64
- selection?: CodexAppServerSkillSelection;
65
- userSkillsRoot?: string;
66
- }): CodexAppServerSkillsListWarmup | undefined;
67
57
  export declare function buildCodexAppServerTurnInput(options: {
68
58
  prompt: string;
69
59
  skills?: CodexAppServerSkillInput[];
@@ -76,6 +66,7 @@ export declare function buildCodexAppServerContextRuntimeHint(options: {
76
66
  expiresAt?: string;
77
67
  }): string;
78
68
  export declare function resolveCodexAppServerContextRuntimeHint(options: {
69
+ activityRef?: ActivityRef;
79
70
  selectionName?: CodexAppServerSkillSelectionName;
80
71
  agentContext?: CodexAppServerAgentContextRuntime;
81
72
  }): CodexAppServerContextRuntimeHint | undefined;
@@ -1,4 +1,4 @@
1
- import { existsSync, statSync } from "node:fs";
1
+ import { existsSync, realpathSync, statSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { listUserSkills } from "../../../skills/index.js";
@@ -30,6 +30,7 @@ export const CODEX_APP_SERVER_SKILL_SELECTIONS = {
30
30
  "task:read",
31
31
  "references:list",
32
32
  "references:read",
33
+ "references:external",
33
34
  "skills:list",
34
35
  "skills:read",
35
36
  ],
@@ -42,6 +43,7 @@ export const CODEX_APP_SERVER_SKILL_SELECTIONS = {
42
43
  "project-docs:read",
43
44
  "references:list",
44
45
  "references:read",
46
+ "references:external",
45
47
  ],
46
48
  },
47
49
  task_compile: {
@@ -54,6 +56,7 @@ export const CODEX_APP_SERVER_SKILL_SELECTIONS = {
54
56
  "task:read",
55
57
  "references:list",
56
58
  "references:read",
59
+ "references:external",
57
60
  ],
58
61
  },
59
62
  context_sync: {
@@ -74,23 +77,29 @@ export const CODEX_APP_SERVER_SKILL_SELECTIONS = {
74
77
  contextApiScopes: [],
75
78
  },
76
79
  workspace_write_run: {
77
- builtInSkillNames: ["read-project-docs", "read-worklist"],
80
+ builtInSkillNames: ["read-project-docs", "read-worklist", "read-references"],
78
81
  userSkillMode: "all_enabled",
79
82
  contextApiScopes: [
80
83
  "project-docs:list",
81
84
  "project-docs:read",
82
85
  "worklist:read",
83
86
  "task:read",
87
+ "references:list",
88
+ "references:read",
89
+ "references:external",
84
90
  ],
85
91
  },
86
92
  follow_up_run: {
87
- builtInSkillNames: ["read-project-docs", "read-worklist"],
93
+ builtInSkillNames: ["read-project-docs", "read-worklist", "read-references"],
88
94
  userSkillMode: "all_enabled",
89
95
  contextApiScopes: [
90
96
  "project-docs:list",
91
97
  "project-docs:read",
92
98
  "worklist:read",
93
99
  "task:read",
100
+ "references:list",
101
+ "references:read",
102
+ "references:external",
94
103
  ],
95
104
  },
96
105
  };
@@ -112,12 +121,12 @@ function resolveBuiltInSkill(options) {
112
121
  if (!statSync(skillMdPath).isFile()) {
113
122
  throw new CodexAppServerSkillSelectionError("built_in_skill_invalid", `Built-in skill is invalid: ${options.name}`);
114
123
  }
115
- return { name: options.name, path };
124
+ return { name: options.name, path: realpathSync(skillMdPath) };
116
125
  }
117
126
  function resolveUserSkill(options) {
118
127
  return {
119
128
  name: options.name,
120
- path: join(options.userSkillsRoot, options.name),
129
+ path: realpathSync(join(options.userSkillsRoot, options.name, "SKILL.md")),
121
130
  };
122
131
  }
123
132
  export function resolveCodexAppServerSkillSelection(selectionName) {
@@ -140,32 +149,6 @@ export function resolveCodexAppServerSkillInputs(options) {
140
149
  : [];
141
150
  return [...builtInSkills, ...userSkills];
142
151
  }
143
- export function resolveCodexAppServerSkillsListWarmup(options) {
144
- const selection = options.selection ??
145
- (options.selectionName === undefined
146
- ? {
147
- builtInSkillNames: [],
148
- userSkillMode: "disabled",
149
- contextApiScopes: [],
150
- }
151
- : resolveCodexAppServerSkillSelection(options.selectionName));
152
- if (selection.userSkillMode !== "all_enabled" || options.userSkillsRoot === undefined) {
153
- return undefined;
154
- }
155
- if (listUserSkills(options.userSkillsRoot).length === 0) {
156
- return undefined;
157
- }
158
- return {
159
- cwds: [options.cwd],
160
- forceReload: true,
161
- perCwdExtraUserRoots: [
162
- {
163
- cwd: options.cwd,
164
- extraUserRoots: [options.userSkillsRoot],
165
- },
166
- ],
167
- };
168
- }
169
152
  function createTextInputItem(text) {
170
153
  return {
171
154
  type: "text",
@@ -206,6 +189,7 @@ export function resolveCodexAppServerContextRuntimeHint(options) {
206
189
  return undefined;
207
190
  }
208
191
  const issued = options.agentContext.issueToken({
192
+ ...(options.activityRef === undefined ? {} : { activityRef: options.activityRef }),
209
193
  selectionName: options.selectionName,
210
194
  scopes: selection.contextApiScopes,
211
195
  });
@@ -218,6 +202,7 @@ export function resolveCodexAppServerContextRuntimeHint(options) {
218
202
  }),
219
203
  sensitiveValues: [issued.token, options.agentContext.baseUrl],
220
204
  scopes: selection.contextApiScopes,
205
+ ...(issued.invocation_ref === undefined ? {} : { invocation_ref: issued.invocation_ref }),
221
206
  };
222
207
  }
223
208
  //# sourceMappingURL=skills.js.map