@xfey/tutti 0.1.108 → 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.
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` 后的项目绑定、恢复和接管入口
@@ -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
@@ -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 {
@@ -167,6 +171,13 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
167
171
  options.builtInSkillsRoot ?? "",
168
172
  ]));
169
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
+ });
170
181
  const model = options.defaultModel ?? DEFAULT_OPENAI_MODEL;
171
182
  const tempRoot = mkdtempSync(join(tmpdir(), "tutti-codex-readonly-procedure-"));
172
183
  const codexHome = options.session?.codexHome ?? join(tempRoot, "codex-home");
@@ -223,7 +234,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
223
234
  options.userSkillsRoot ?? "",
224
235
  options.builtInSkillsRoot ?? "",
225
236
  ...(contextRuntimeHint?.sensitiveValues ?? []),
226
- ...skillInputs.map((skill) => skill.path),
237
+ ...skillInputs.flatMap((skill) => [skill.path, dirname(skill.path)]),
227
238
  ];
228
239
  const strictLeakCheckValues = [options.apiKey, ...(contextRuntimeHint?.sensitiveValues ?? [])];
229
240
  let primaryError;
@@ -248,6 +259,16 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
248
259
  if (!isApiKeyAccount(loginResponse)) {
249
260
  throw new CodexAppServerReadOnlyProcedureError("auth_failed", "Codex app-server did not accept API key auth.");
250
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
+ });
251
272
  let threadId;
252
273
  if (options.session?.threadId === undefined) {
253
274
  const threadStartResponse = await client.request("thread/start", {
@@ -298,6 +319,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
298
319
  modelProvider,
299
320
  outputSchema: options.outputSchema,
300
321
  }, requestTimeoutMs);
322
+ reportProviderInvocationProgress(options.onProgress, { stage: "turn_started" });
301
323
  const turnCompletedPromise = client.waitForNotification((notification) => notification.method === "turn/completed", turnTimeoutMs);
302
324
  const serverRequestPromise = client.waitForServerRequest(turnTimeoutMs);
303
325
  let completedOrRequest;
@@ -350,6 +372,10 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
350
372
  const agentText = client.notifications
351
373
  .map((notification) => extractAgentText(notification))
352
374
  .join("");
375
+ reportProviderInvocationProgress(options.onProgress, {
376
+ stage: "turn_completed",
377
+ completed_tool_count: structuredOutputDetails(client, agentText).completed_tool_count ?? 0,
378
+ });
353
379
  let parsed;
354
380
  try {
355
381
  parsed = parseCodexAppServerStructuredJson(agentText);
@@ -408,6 +434,9 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
408
434
  if (error instanceof CodexAppServerReadOnlyProcedureError) {
409
435
  primaryError = error;
410
436
  }
437
+ else if (error instanceof CodexAppServerSkillSelectionError) {
438
+ primaryError = new CodexAppServerReadOnlyProcedureError("skill_selection_failed", error.message);
439
+ }
411
440
  else if (error instanceof CodexAppServerProtocolError) {
412
441
  primaryError = new CodexAppServerReadOnlyProcedureError("app_server_protocol_error", redactWithKnownValues(error.message, sensitiveValues));
413
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
@@ -30,27 +30,21 @@ export type CodexAppServerAgentContextTokenRequest = {
30
30
  export type CodexAppServerAgentContextToken = {
31
31
  token: string;
32
32
  expires_at?: string;
33
+ invocation_ref?: string;
33
34
  };
34
35
  export type CodexAppServerAgentContextRuntime = {
35
36
  baseUrl: string;
36
37
  issueToken: (input: CodexAppServerAgentContextTokenRequest) => CodexAppServerAgentContextToken;
37
38
  };
38
- export type CodexAppServerSkillsListWarmup = {
39
- cwds: string[];
40
- forceReload: true;
41
- perCwdExtraUserRoots: Array<{
42
- cwd: string;
43
- extraUserRoots: string[];
44
- }>;
45
- };
46
39
  export type CodexAppServerContextRuntimeHint = {
47
40
  text: string;
48
41
  sensitiveValues: string[];
49
42
  scopes: CodexAppServerContextApiScope[];
43
+ invocation_ref?: string;
50
44
  };
51
45
  export declare class CodexAppServerSkillSelectionError extends Error {
52
- readonly code: "built_in_skill_not_found" | "built_in_skill_invalid";
53
- 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);
54
48
  }
55
49
  export declare const CODEX_APP_SERVER_SKILL_SELECTIONS: Record<CodexAppServerSkillSelectionName, CodexAppServerSkillSelection>;
56
50
  export declare function resolveCodexAppServerSkillSelection(selectionName: CodexAppServerSkillSelectionName): CodexAppServerSkillSelection;
@@ -60,12 +54,6 @@ export declare function resolveCodexAppServerSkillInputs(options: {
60
54
  builtInSkillsRoot?: string;
61
55
  userSkillsRoot?: string;
62
56
  }): CodexAppServerSkillInput[];
63
- export declare function resolveCodexAppServerSkillsListWarmup(options: {
64
- cwd: string;
65
- selectionName?: CodexAppServerSkillSelectionName;
66
- selection?: CodexAppServerSkillSelection;
67
- userSkillsRoot?: string;
68
- }): CodexAppServerSkillsListWarmup | undefined;
69
57
  export declare function buildCodexAppServerTurnInput(options: {
70
58
  prompt: string;
71
59
  skills?: CodexAppServerSkillInput[];
@@ -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";
@@ -121,12 +121,12 @@ function resolveBuiltInSkill(options) {
121
121
  if (!statSync(skillMdPath).isFile()) {
122
122
  throw new CodexAppServerSkillSelectionError("built_in_skill_invalid", `Built-in skill is invalid: ${options.name}`);
123
123
  }
124
- return { name: options.name, path };
124
+ return { name: options.name, path: realpathSync(skillMdPath) };
125
125
  }
126
126
  function resolveUserSkill(options) {
127
127
  return {
128
128
  name: options.name,
129
- path: join(options.userSkillsRoot, options.name),
129
+ path: realpathSync(join(options.userSkillsRoot, options.name, "SKILL.md")),
130
130
  };
131
131
  }
132
132
  export function resolveCodexAppServerSkillSelection(selectionName) {
@@ -149,32 +149,6 @@ export function resolveCodexAppServerSkillInputs(options) {
149
149
  : [];
150
150
  return [...builtInSkills, ...userSkills];
151
151
  }
152
- export function resolveCodexAppServerSkillsListWarmup(options) {
153
- const selection = options.selection ??
154
- (options.selectionName === undefined
155
- ? {
156
- builtInSkillNames: [],
157
- userSkillMode: "disabled",
158
- contextApiScopes: [],
159
- }
160
- : resolveCodexAppServerSkillSelection(options.selectionName));
161
- if (selection.userSkillMode !== "all_enabled" || options.userSkillsRoot === undefined) {
162
- return undefined;
163
- }
164
- if (listUserSkills(options.userSkillsRoot).length === 0) {
165
- return undefined;
166
- }
167
- return {
168
- cwds: [options.cwd],
169
- forceReload: true,
170
- perCwdExtraUserRoots: [
171
- {
172
- cwd: options.cwd,
173
- extraUserRoots: [options.userSkillsRoot],
174
- },
175
- ],
176
- };
177
- }
178
152
  function createTextInputItem(text) {
179
153
  return {
180
154
  type: "text",
@@ -228,6 +202,7 @@ export function resolveCodexAppServerContextRuntimeHint(options) {
228
202
  }),
229
203
  sensitiveValues: [issued.token, options.agentContext.baseUrl],
230
204
  scopes: selection.contextApiScopes,
205
+ ...(issued.invocation_ref === undefined ? {} : { invocation_ref: issued.invocation_ref }),
231
206
  };
232
207
  }
233
208
  //# sourceMappingURL=skills.js.map
@@ -1,7 +1,8 @@
1
1
  import { 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";
5
6
  import { buildCodexAppServerProcessPlan } from "../codex-app-server.js";
6
7
  import { DEFAULT_OPENAI_MODEL } from "../model-config.js";
7
8
  import { CodexAppServerJsonRpcClient, CodexAppServerProtocolError, } from "./json-rpc.js";
@@ -10,7 +11,7 @@ import { CodexAppServerAdmissionError, } from "./invocation-coordinator.js";
10
11
  import { createCodexAppServerRuntimeObserver, } from "./runtime-telemetry.js";
11
12
  import { createReadOnlySandboxPolicy, createWorkspaceWriteSandboxPolicy, } from "./sandbox-policy.js";
12
13
  import { buildCodexAppServerProviderConfigOverrides, resolveCodexAppServerModelProvider, } from "./provider-config.js";
13
- import { buildCodexAppServerTurnInput, resolveCodexAppServerContextRuntimeHint, resolveCodexAppServerSkillsListWarmup, resolveCodexAppServerSkillInputs, CodexAppServerSkillSelectionError, } from "./skills.js";
14
+ import { buildCodexAppServerTurnInput, resolveCodexAppServerContextRuntimeHint, resolveCodexAppServerSkillInputs, CodexAppServerSkillSelectionError, } from "./skills.js";
14
15
  import { compactText, containsKnownSecret, createCodexSpawnEnv, extractAgentText, extractServerRequestMethod, findJsonObjectCandidates, isApiKeyAccount, readThreadId, readTurnError, readTurnStatus, redactWithKnownValues, } from "./runtime-helpers.js";
15
16
  import { extractCodexAppServerTokenUsage } from "../token-usage.js";
16
17
  const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
@@ -170,7 +171,6 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
170
171
  }
171
172
  }
172
173
  let skillInputs;
173
- let skillsListWarmup;
174
174
  let contextRuntimeHint;
175
175
  try {
176
176
  skillInputs = resolveCodexAppServerSkillInputs({
@@ -182,13 +182,6 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
182
182
  ? {}
183
183
  : { builtInSkillsRoot: options.builtInSkillsRoot }),
184
184
  });
185
- skillsListWarmup = resolveCodexAppServerSkillsListWarmup({
186
- cwd: options.workspaceRoot,
187
- ...(options.skillSelectionName === undefined
188
- ? {}
189
- : { selectionName: options.skillSelectionName }),
190
- ...(options.userSkillsRoot === undefined ? {} : { userSkillsRoot: options.userSkillsRoot }),
191
- });
192
185
  contextRuntimeHint = resolveCodexAppServerContextRuntimeHint({
193
186
  ...(options.approvalContext?.activityRef === undefined
194
187
  ? {}
@@ -267,11 +260,7 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
267
260
  options.userSkillsRoot ?? "",
268
261
  options.builtInSkillsRoot ?? "",
269
262
  ...(contextRuntimeHint?.sensitiveValues ?? []),
270
- ...skillInputs.map((skill) => skill.path),
271
- ...(skillsListWarmup?.perCwdExtraUserRoots.flatMap((entry) => [
272
- entry.cwd,
273
- ...entry.extraUserRoots,
274
- ]) ?? []),
263
+ ...skillInputs.flatMap((skill) => [skill.path, dirname(skill.path)]),
275
264
  ];
276
265
  const strictLeakCheckValues = [options.apiKey, ...(contextRuntimeHint?.sensitiveValues ?? [])];
277
266
  let primaryError;
@@ -297,9 +286,12 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
297
286
  if (!isApiKeyAccount(loginResponse)) {
298
287
  throw new CodexAppServerWorkspaceWriteRunError("auth_failed", "Codex app-server did not accept API key auth.");
299
288
  }
300
- if (skillsListWarmup !== undefined) {
301
- await client.request("skills/list", skillsListWarmup, requestTimeoutMs);
302
- }
289
+ await discoverCodexAppServerSkills({
290
+ client,
291
+ cwd: options.workspaceRoot,
292
+ skills: skillInputs,
293
+ requestTimeoutMs,
294
+ });
303
295
  let threadId;
304
296
  if (options.session?.threadId === undefined) {
305
297
  const threadStartResponse = await client.request("thread/start", {
@@ -484,7 +476,9 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
484
476
  }
485
477
  else {
486
478
  const message = error instanceof Error ? error.message : String(error);
487
- primaryError = new CodexAppServerWorkspaceWriteRunError("app_server_protocol_error", redactWithKnownValues(message, sensitiveValues), { turn_start_state: turnStartState });
479
+ primaryError = new CodexAppServerWorkspaceWriteRunError(error instanceof CodexAppServerSkillSelectionError
480
+ ? "skill_selection_failed"
481
+ : "app_server_protocol_error", redactWithKnownValues(message, sensitiveValues), { turn_start_state: turnStartState });
488
482
  }
489
483
  }
490
484
  finally {
@@ -1,12 +1,14 @@
1
1
  import { CHAT_ASSISTANT_OUTPUT_SCHEMA, isChatAssistantOutput, } from "../../chat-assistant/index.js";
2
2
  import { readOpenAiApiKeyCredential } from "./credential-store.js";
3
- import { resolveOpenAiProviderConfig, } from "./provider-config.js";
4
- import { runCodexAppServerReadOnlyProcedure } from "./app-server/read-only-procedure.js";
3
+ import { resolveOpenAiProviderConfig } from "./provider-config.js";
4
+ import { CodexAppServerReadOnlyProcedureError, runCodexAppServerReadOnlyProcedure, } from "./app-server/read-only-procedure.js";
5
+ import { reportProviderInvocationProgress } from "../invocation-progress.js";
5
6
  import { getProjectUserSkillsRoot } from "../../skills/index.js";
6
7
  export function createOpenAiReadOnlyChatAssistantModel(options) {
7
8
  return {
8
9
  async answer(input) {
9
10
  const result = await runCodexAppServerReadOnlyProcedure({
11
+ ...(input.onProgress === undefined ? {} : { onProgress: input.onProgress }),
10
12
  apiKey: options.apiKey,
11
13
  workspaceRoot: options.workspaceRoot,
12
14
  prompt: input.prompt,
@@ -37,6 +39,14 @@ export function createOpenAiReadOnlyChatAssistantModel(options) {
37
39
  ...(options.onLifecycleIncident === undefined
38
40
  ? {}
39
41
  : { onLifecycleIncident: options.onLifecycleIncident }),
42
+ }).catch((error) => {
43
+ reportProviderInvocationProgress(input.onProgress, {
44
+ stage: "provider_failed",
45
+ reason_code: error instanceof CodexAppServerReadOnlyProcedureError
46
+ ? error.code
47
+ : "provider_unavailable",
48
+ });
49
+ throw error;
40
50
  });
41
51
  if (options.recordUsage !== undefined && result.usage !== undefined) {
42
52
  options.recordUsage({
@@ -137,6 +137,7 @@ function createAgentContextRuntime(options) {
137
137
  return {
138
138
  token: token.token,
139
139
  expires_at: token.expires_at,
140
+ invocation_ref: token.reference_invocation_ref,
140
141
  };
141
142
  },
142
143
  };
@@ -419,6 +420,7 @@ export async function startForegroundHostServer(options) {
419
420
  }),
420
421
  });
421
422
  const chatAssistant = new ReadOnlyChatAssistant({
423
+ logger: createHostRuntimeLogger({ component: "chat-assistant", logFilePath, now }),
422
424
  project: chatAssistantProject,
423
425
  store,
424
426
  events: workspaceEvents,
@@ -16,20 +16,43 @@ export function registerAgentContextExternalReferenceRoutes(app, options) {
16
16
  }, async (request, reply) => {
17
17
  reply.header("cache-control", "no-store");
18
18
  const invocation = authorizeAgentContextRequest(request, options, "references:external");
19
+ const started = performance.now();
20
+ const diagnosticFields = {
21
+ action,
22
+ invocation_ref: invocation.reference_invocation_ref,
23
+ activity_ref: invocation.activity_ref,
24
+ };
25
+ request.log.info({ ...diagnosticFields, stage: "external_reference_request" }, "External Reference request started");
19
26
  const service = options.externalReferences;
20
- if (!service)
27
+ if (!service) {
28
+ request.log.info({
29
+ ...diagnosticFields,
30
+ stage: "external_reference_unavailable",
31
+ reason_code: "external_references_not_enabled",
32
+ }, "External Reference service unavailable");
21
33
  return { kind: "unavailable", reason: "external_references_not_enabled" };
34
+ }
22
35
  const sourceRef = request.query.source_ref;
23
36
  if (action === "read" &&
24
37
  (sourceRef === undefined || !/^external_ref_[a-f0-9]{32}$/u.test(sourceRef)))
25
38
  return { kind: "unavailable", reason: "reference_source_invalid" };
26
39
  try {
27
- if (action === "list")
28
- return { items: await service.list(invocation) };
40
+ if (action === "list") {
41
+ const items = await service.list(invocation);
42
+ request.log.info({
43
+ ...diagnosticFields,
44
+ stage: "external_reference_list",
45
+ source_count: items.length,
46
+ elapsed_ms: Math.round(performance.now() - started),
47
+ }, "External Reference sources listed");
48
+ return { items };
49
+ }
29
50
  const result = await service.read(invocation, sourceRef);
30
51
  const { copy } = result;
31
52
  request.log.info({
32
53
  stage: "external_reference_read",
54
+ action,
55
+ elapsed_ms: Math.round(performance.now() - started),
33
56
  invocation_ref: invocation.reference_invocation_ref,
34
57
  activity_ref: invocation.activity_ref,
35
58
  source_ref: copy.source_ref,
@@ -70,7 +93,9 @@ export function registerAgentContextExternalReferenceRoutes(app, options) {
70
93
  ? "reference_cache_unsafe"
71
94
  : "reference_read_unavailable";
72
95
  request.log.warn({
73
- stage: "external_reference_read",
96
+ stage: "external_reference_unavailable",
97
+ action,
98
+ elapsed_ms: Math.round(performance.now() - started),
74
99
  invocation_ref: invocation.reference_invocation_ref,
75
100
  activity_ref: invocation.activity_ref,
76
101
  ...(sourceRef === undefined ? {} : { source_ref: sourceRef }),
@@ -3,18 +3,19 @@ import { join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import fastifyStatic from "@fastify/static";
5
5
  const BLOCKED_SPA_FALLBACK_PREFIXES = [
6
- "/api/",
6
+ "/api",
7
7
  "/health",
8
- "/host-local/",
9
- "/local-console/",
10
- "/assets/",
8
+ "/host-local",
9
+ "/local-console",
10
+ "/assets",
11
+ "/internal",
11
12
  ];
12
13
  function requestPath(request) {
13
14
  const queryIndex = request.url.indexOf("?");
14
15
  return queryIndex === -1 ? request.url : request.url.slice(0, queryIndex);
15
16
  }
16
17
  function shouldServeSpaFallback(path) {
17
- return !BLOCKED_SPA_FALLBACK_PREFIXES.some((prefix) => path === prefix || path.startsWith(prefix));
18
+ return !BLOCKED_SPA_FALLBACK_PREFIXES.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
18
19
  }
19
20
  function sendIndex(reply, indexHtml) {
20
21
  void reply
@@ -39,7 +40,10 @@ export function registerHostWebStaticRoutes(app, options) {
39
40
  });
40
41
  app.get("/*", (request, reply) => {
41
42
  if (!shouldServeSpaFallback(requestPath(request))) {
42
- void reply.status(404).send({
43
+ void reply
44
+ .header("cache-control", "no-store")
45
+ .status(404)
46
+ .send({
43
47
  error: {
44
48
  code: "not_found",
45
49
  message: "Route was not found",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfey/tutti",
3
- "version": "0.1.108",
3
+ "version": "0.1.109",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",