@xfey/tutti 0.1.78 → 0.1.80

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 (85) hide show
  1. package/README.md +1 -1
  2. package/dist/chat-assistant/index.d.ts +11 -1
  3. package/dist/chat-assistant/index.js +61 -7
  4. package/dist/checks/index.d.ts +22 -0
  5. package/dist/checks/index.js +110 -4
  6. package/dist/control-plane/follow-up-start.d.ts +1 -0
  7. package/dist/control-plane/follow-up-start.js +3 -1
  8. package/dist/control-plane/index.d.ts +6 -1
  9. package/dist/control-plane/index.js +72 -11
  10. package/dist/control-plane/procedure-logging.js +24 -67
  11. package/dist/control-plane/project-context-bootstrap.js +7 -1
  12. package/dist/control-plane/reference-summary-refresh.js +1 -1
  13. package/dist/control-plane/run-result-recording.js +2 -1
  14. package/dist/control-plane/task-compile-start.d.ts +1 -0
  15. package/dist/control-plane/task-compile-start.js +11 -1
  16. package/dist/control-plane/workflows/openai.d.ts +6 -0
  17. package/dist/control-plane/workflows/openai.js +16 -3
  18. package/dist/procedure-engine/index.d.ts +1 -0
  19. package/dist/procedure-engine/index.js +7 -1
  20. package/dist/process-tree.d.ts +25 -0
  21. package/dist/process-tree.js +121 -0
  22. package/dist/providers/openai/app-server/invocation-coordinator.d.ts +28 -0
  23. package/dist/providers/openai/app-server/invocation-coordinator.js +104 -0
  24. package/dist/providers/openai/app-server/invocation-lifecycle.d.ts +45 -0
  25. package/dist/providers/openai/app-server/invocation-lifecycle.js +104 -0
  26. package/dist/providers/openai/app-server/json-rpc.d.ts +1 -1
  27. package/dist/providers/openai/app-server/json-rpc.js +9 -16
  28. package/dist/providers/openai/app-server/read-only-procedure.d.ts +8 -2
  29. package/dist/providers/openai/app-server/read-only-procedure.js +63 -11
  30. package/dist/providers/openai/app-server/smoke-types.d.ts +4 -2
  31. package/dist/providers/openai/app-server/smoke.js +41 -12
  32. package/dist/providers/openai/app-server/stale-temp-cleanup.d.ts +17 -0
  33. package/dist/providers/openai/app-server/stale-temp-cleanup.js +130 -0
  34. package/dist/providers/openai/app-server/workspace-write-run.d.ts +8 -2
  35. package/dist/providers/openai/app-server/workspace-write-run.js +63 -11
  36. package/dist/providers/openai/chat-assistant.d.ts +6 -0
  37. package/dist/providers/openai/chat-assistant.js +12 -0
  38. package/dist/providers/openai/index.d.ts +1 -0
  39. package/dist/providers/openai/index.js +1 -0
  40. package/dist/providers/openai/machine-local-files.d.ts +1 -1
  41. package/dist/providers/openai/machine-local-files.js +32 -9
  42. package/dist/providers/openai/provider-config.d.ts +5 -1
  43. package/dist/providers/openai/provider-config.js +25 -4
  44. package/dist/providers/openai/provider-models.d.ts +41 -0
  45. package/dist/providers/openai/provider-models.js +82 -0
  46. package/dist/providers/openai/provider-setup.js +5 -2
  47. package/dist/run-engine/index.js +2 -1
  48. package/dist/run-pipeline/artifact-applicability.js +2 -2
  49. package/dist/run-pipeline/openai.d.ts +9 -0
  50. package/dist/run-pipeline/openai.js +26 -26
  51. package/dist/run-pipeline/task-run-invocation.js +6 -0
  52. package/dist/runtime-failures.d.ts +3 -0
  53. package/dist/runtime-failures.js +22 -0
  54. package/dist/server-shell/cli/host-lifecycle.js +1 -1
  55. package/dist/server-shell/cli/host-project-runtime-lifecycle.d.ts +43 -0
  56. package/dist/server-shell/cli/host-project-runtime-lifecycle.js +54 -0
  57. package/dist/server-shell/cli/host-server-runtime.d.ts +1 -1
  58. package/dist/server-shell/cli/host-server-runtime.js +140 -15
  59. package/dist/server-shell/http/create-server.d.ts +3 -0
  60. package/dist/server-shell/http/create-server.js +10 -4
  61. package/dist/server-shell/http/routes/project-api/invalidations.d.ts +1 -0
  62. package/dist/server-shell/http/routes/project-api/invalidations.js +4 -5
  63. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +38 -14
  64. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.js +26 -1
  65. package/dist/server-shell/http/routes/project-api/openapi.d.ts +38 -14
  66. package/dist/server-shell/http/routes/project-api/schemas.d.ts +8 -2
  67. package/dist/server-shell/http/routes/project-api/schemas.js +2 -0
  68. package/dist/server-shell/http/routes/project-api/session-requirements.d.ts +1 -0
  69. package/dist/server-shell/http/routes/project-api/session-requirements.js +7 -0
  70. package/dist/server-shell/http/routes/project-api/system-routes.js +96 -5
  71. package/dist/server-shell/http/routes/project-api/types.d.ts +10 -1
  72. package/dist/server-shell/http/routes/project-api/workspace-projections.js +4 -2
  73. package/dist/server-shell/http/routes/project-api.d.ts +1 -1
  74. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  75. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  76. package/node_modules/@tutti/shared/dist/schemas/api/provider-config.d.ts +36 -0
  77. package/node_modules/@tutti/shared/dist/schemas/api/provider-config.js +58 -1
  78. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +9 -1
  79. package/package.json +1 -1
  80. package/web/assets/{homepage-motion-scene-sNI8fBH3.js → homepage-motion-scene-DXzmjALa.js} +1 -1
  81. package/web/assets/index-9gApbtPC.js +69 -0
  82. package/web/assets/index-BKeSEhsn.css +1 -0
  83. package/web/index.html +2 -2
  84. package/web/assets/index-BUfAlA_H.css +0 -1
  85. package/web/assets/index-d-q9yZ09.js +0 -69
@@ -2,6 +2,8 @@ import type { ProjectId } from "@tutti/shared/ids";
2
2
  import { type ReadOnlyChatAssistantModel } from "../../chat-assistant/index.js";
3
3
  import { type OpenAiProviderConfig } from "./provider-config.js";
4
4
  import type { CodexAppServerAgentContextRuntime } from "./app-server/skills.js";
5
+ import type { CodexAppServerInvocationCoordinator } from "./app-server/invocation-coordinator.js";
6
+ import type { CodexAppServerLifecycleIncident } from "./app-server/invocation-lifecycle.js";
5
7
  import type { ProviderUsageRecorder } from "../../provider-usage/index.js";
6
8
  export type OpenAiReadOnlyChatAssistantModelOptions = {
7
9
  config: OpenAiProviderConfig;
@@ -16,6 +18,8 @@ export type OpenAiReadOnlyChatAssistantModelOptions = {
16
18
  turnTimeoutMs?: number;
17
19
  baseEnv?: NodeJS.ProcessEnv;
18
20
  recordUsage?: ProviderUsageRecorder;
21
+ invocationCoordinator?: CodexAppServerInvocationCoordinator;
22
+ onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
19
23
  };
20
24
  export type ProjectOpenAiReadOnlyChatAssistantModelOptions = {
21
25
  tuttiHome: string;
@@ -23,6 +27,8 @@ export type ProjectOpenAiReadOnlyChatAssistantModelOptions = {
23
27
  workspaceRoot: string;
24
28
  agentContext?: CodexAppServerAgentContextRuntime;
25
29
  recordUsage?: ProviderUsageRecorder;
30
+ invocationCoordinator?: CodexAppServerInvocationCoordinator;
31
+ onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
26
32
  };
27
33
  export type ReadOnlyChatAssistantModelResolution = {
28
34
  kind: "ready";
@@ -31,6 +31,12 @@ export function createOpenAiReadOnlyChatAssistantModel(options) {
31
31
  : { requestTimeoutMs: options.requestTimeoutMs }),
32
32
  ...(options.turnTimeoutMs === undefined ? {} : { turnTimeoutMs: options.turnTimeoutMs }),
33
33
  ...(options.baseEnv === undefined ? {} : { baseEnv: options.baseEnv }),
34
+ ...(options.invocationCoordinator === undefined
35
+ ? {}
36
+ : { invocationCoordinator: options.invocationCoordinator }),
37
+ ...(options.onLifecycleIncident === undefined
38
+ ? {}
39
+ : { onLifecycleIncident: options.onLifecycleIncident }),
34
40
  });
35
41
  if (options.recordUsage !== undefined && result.usage !== undefined) {
36
42
  options.recordUsage({
@@ -65,6 +71,12 @@ export function resolveProjectOpenAiReadOnlyChatAssistantModel(options) {
65
71
  userSkillsRoot: getProjectUserSkillsRoot(options.tuttiHome, options.projectId),
66
72
  ...(options.agentContext === undefined ? {} : { agentContext: options.agentContext }),
67
73
  ...(options.recordUsage === undefined ? {} : { recordUsage: options.recordUsage }),
74
+ ...(options.invocationCoordinator === undefined
75
+ ? {}
76
+ : { invocationCoordinator: options.invocationCoordinator }),
77
+ ...(options.onLifecycleIncident === undefined
78
+ ? {}
79
+ : { onLifecycleIncident: options.onLifecycleIncident }),
68
80
  }),
69
81
  };
70
82
  }
@@ -8,6 +8,7 @@ export * from "./credential-store.js";
8
8
  export * from "./credential-validation.js";
9
9
  export * from "./model-config.js";
10
10
  export * from "./provider-config.js";
11
+ export * from "./provider-models.js";
11
12
  export * from "./provider-setup.js";
12
13
  export * from "./sdk-procedure-runner.js";
13
14
  export * from "./token-usage.js";
@@ -8,6 +8,7 @@ export * from "./credential-store.js";
8
8
  export * from "./credential-validation.js";
9
9
  export * from "./model-config.js";
10
10
  export * from "./provider-config.js";
11
+ export * from "./provider-models.js";
11
12
  export * from "./provider-setup.js";
12
13
  export * from "./sdk-procedure-runner.js";
13
14
  export * from "./token-usage.js";
@@ -8,7 +8,7 @@ export type JsonFileReadResult = {
8
8
  };
9
9
  export declare function ensurePrivateDirectory(directoryPath: string): void;
10
10
  export declare function writePrivateJsonFile(filePath: string, value: unknown): void;
11
- export declare function writePrivateTextFile(filePath: string, value: string): void;
11
+ export declare function writePrivateTextFileAtomically(filePath: string, value: string): void;
12
12
  export declare function readJsonFile(filePath: string): JsonFileReadResult;
13
13
  export declare function readTextFile(filePath: string): {
14
14
  kind: "missing";
@@ -1,5 +1,6 @@
1
- import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname } from "node:path";
1
+ import { chmodSync, closeSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { randomBytes } from "node:crypto";
3
+ import { basename, dirname, join } from "node:path";
3
4
  function isErrnoException(error) {
4
5
  return error instanceof Error && "code" in error;
5
6
  }
@@ -15,13 +16,35 @@ export function writePrivateJsonFile(filePath, value) {
15
16
  });
16
17
  chmodSync(filePath, 0o600);
17
18
  }
18
- export function writePrivateTextFile(filePath, value) {
19
- ensurePrivateDirectory(dirname(filePath));
20
- writeFileSync(filePath, value, {
21
- encoding: "utf8",
22
- mode: 0o600,
23
- });
24
- chmodSync(filePath, 0o600);
19
+ export function writePrivateTextFileAtomically(filePath, value) {
20
+ const directoryPath = dirname(filePath);
21
+ ensurePrivateDirectory(directoryPath);
22
+ const temporaryPath = join(directoryPath, `.${basename(filePath)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
23
+ let descriptor;
24
+ try {
25
+ descriptor = openSync(temporaryPath, "wx", 0o600);
26
+ writeFileSync(descriptor, value, { encoding: "utf8" });
27
+ closeSync(descriptor);
28
+ descriptor = undefined;
29
+ renameSync(temporaryPath, filePath);
30
+ }
31
+ catch (error) {
32
+ if (descriptor !== undefined) {
33
+ try {
34
+ closeSync(descriptor);
35
+ }
36
+ catch {
37
+ // Preserve the original write or close failure.
38
+ }
39
+ }
40
+ try {
41
+ unlinkSync(temporaryPath);
42
+ }
43
+ catch {
44
+ // The temporary file may already have been renamed or never created.
45
+ }
46
+ throw error;
47
+ }
25
48
  }
26
49
  export function readJsonFile(filePath) {
27
50
  let raw;
@@ -3,7 +3,7 @@ import { type OpenAiCredentialRef } from "./credential-store.js";
3
3
  import type { OPENAI_API_KEY_AUTH_KIND, OPENAI_PROVIDER } from "./model-config.js";
4
4
  import { OpenAiProviderConfigError } from "./provider-config-errors.js";
5
5
  export { OpenAiProviderConfigError } from "./provider-config-errors.js";
6
- export { parseOpenAiProviderConfig, } from "./provider-config-shape.js";
6
+ export { parseOpenAiProviderConfig } from "./provider-config-shape.js";
7
7
  export type OpenAiProviderConfig = {
8
8
  provider: typeof OPENAI_PROVIDER;
9
9
  auth_kind: typeof OPENAI_API_KEY_AUTH_KIND;
@@ -58,6 +58,10 @@ export type OpenAiProviderConfigResolution = {
58
58
  };
59
59
  export declare function resolveTuttiHome(input: string | undefined, cwd?: string): string;
60
60
  export declare function getProjectProviderConfigPath(tuttiHome: string, projectId: ProjectId): string;
61
+ export declare function withProjectOpenAiProviderConfigLock<T>(options: {
62
+ tuttiHome: string;
63
+ projectId: ProjectId;
64
+ }, operation: () => Promise<T>): Promise<T>;
61
65
  export declare function readProjectOpenAiProviderConfig(tuttiHome: string, projectId: ProjectId): ProviderConfigReadResult<OpenAiProviderConfig>;
62
66
  export declare function resolveOpenAiProviderConfig(options: {
63
67
  tuttiHome: string;
@@ -1,12 +1,12 @@
1
1
  import { homedir } from "node:os";
2
2
  import { isAbsolute, join, resolve } from "node:path";
3
- import { openAiCredentialExists, } from "./credential-store.js";
4
- import { readTextFile, writePrivateTextFile } from "./machine-local-files.js";
3
+ import { openAiCredentialExists } from "./credential-store.js";
4
+ import { readTextFile, writePrivateTextFileAtomically } from "./machine-local-files.js";
5
5
  import { OpenAiProviderConfigError } from "./provider-config-errors.js";
6
6
  import { parseOpenAiProviderConfig } from "./provider-config-shape.js";
7
7
  import { formatProjectOpenAiProviderConfig, parseTomlOpenAiSection, parseTuttiConfigToml, } from "./provider-config-toml.js";
8
8
  export { OpenAiProviderConfigError } from "./provider-config-errors.js";
9
- export { parseOpenAiProviderConfig, } from "./provider-config-shape.js";
9
+ export { parseOpenAiProviderConfig } from "./provider-config-shape.js";
10
10
  export function resolveTuttiHome(input, cwd = process.cwd()) {
11
11
  if (input === undefined || input.trim() === "") {
12
12
  return join(homedir(), ".tutti");
@@ -23,6 +23,27 @@ export function resolveTuttiHome(input, cwd = process.cwd()) {
23
23
  export function getProjectProviderConfigPath(tuttiHome, projectId) {
24
24
  return join(tuttiHome, "projects", projectId, "config.toml");
25
25
  }
26
+ const providerConfigMutationTails = new Map();
27
+ export async function withProjectOpenAiProviderConfigLock(options, operation) {
28
+ const key = getProjectProviderConfigPath(options.tuttiHome, options.projectId);
29
+ const previous = providerConfigMutationTails.get(key) ?? Promise.resolve();
30
+ let release = () => { };
31
+ const current = new Promise((resolve) => {
32
+ release = resolve;
33
+ });
34
+ const tail = previous.then(() => current);
35
+ providerConfigMutationTails.set(key, tail);
36
+ await previous;
37
+ try {
38
+ return await operation();
39
+ }
40
+ finally {
41
+ release();
42
+ if (providerConfigMutationTails.get(key) === tail) {
43
+ providerConfigMutationTails.delete(key);
44
+ }
45
+ }
46
+ }
26
47
  export function readProjectOpenAiProviderConfig(tuttiHome, projectId) {
27
48
  const sourcePath = getProjectProviderConfigPath(tuttiHome, projectId);
28
49
  const readResult = readTextFile(sourcePath);
@@ -77,7 +98,7 @@ export function resolveOpenAiProviderConfig(options) {
77
98
  };
78
99
  }
79
100
  export function writeProjectOpenAiProviderConfig(tuttiHome, projectId, config) {
80
- writePrivateTextFile(getProjectProviderConfigPath(tuttiHome, projectId), formatProjectOpenAiProviderConfig(config));
101
+ writePrivateTextFileAtomically(getProjectProviderConfigPath(tuttiHome, projectId), formatProjectOpenAiProviderConfig(config));
81
102
  }
82
103
  export function createRedactedOpenAiProviderConfigProjection(config) {
83
104
  const projection = {
@@ -0,0 +1,41 @@
1
+ import type { ProjectId } from "@tutti/shared/ids";
2
+ import { type OpenAiCredentialValidationFailureReason, type OpenAiCredentialValidationInput, type OpenAiCredentialValidationResult, type OpenAiModelDiscoveryInput, type OpenAiModelDiscoveryResult } from "./credential-validation.js";
3
+ import { type RedactedOpenAiProviderConfigProjection } from "./provider-config.js";
4
+ export type ProjectOpenAiProviderUnavailableReason = "provider_not_configured" | "provider_config_invalid" | "provider_credential_missing";
5
+ export type ProjectOpenAiModelDiscoveryResult = OpenAiModelDiscoveryResult | {
6
+ kind: "unavailable";
7
+ reason: ProjectOpenAiProviderUnavailableReason;
8
+ };
9
+ export type UpdateProjectOpenAiDefaultModelResult = {
10
+ kind: "updated";
11
+ projection: RedactedOpenAiProviderConfigProjection;
12
+ } | {
13
+ kind: "unchanged";
14
+ projection: RedactedOpenAiProviderConfigProjection;
15
+ } | {
16
+ kind: "stale_default_model";
17
+ current_default_model: string;
18
+ } | {
19
+ kind: "validation_failed";
20
+ reason: OpenAiCredentialValidationFailureReason;
21
+ retryable: boolean;
22
+ } | {
23
+ kind: "provider_unavailable";
24
+ reason: ProjectOpenAiProviderUnavailableReason;
25
+ };
26
+ export type DiscoverProjectOpenAiModelsOptions = {
27
+ tuttiHome: string;
28
+ projectId: ProjectId;
29
+ discoverModels?: (input: OpenAiModelDiscoveryInput) => Promise<OpenAiModelDiscoveryResult>;
30
+ };
31
+ export type UpdateProjectOpenAiDefaultModelOptions = {
32
+ tuttiHome: string;
33
+ projectId: ProjectId;
34
+ defaultModel: string;
35
+ expectedDefaultModel: string;
36
+ now?: () => Date;
37
+ validateCredential?: (input: OpenAiCredentialValidationInput) => Promise<OpenAiCredentialValidationResult>;
38
+ };
39
+ export declare function discoverProjectOpenAiModels(options: DiscoverProjectOpenAiModelsOptions): Promise<ProjectOpenAiModelDiscoveryResult>;
40
+ export declare function updateProjectOpenAiDefaultModel(options: UpdateProjectOpenAiDefaultModelOptions): Promise<UpdateProjectOpenAiDefaultModelResult>;
41
+ //# sourceMappingURL=provider-models.d.ts.map
@@ -0,0 +1,82 @@
1
+ import { readOpenAiApiKeyCredential } from "./credential-store.js";
2
+ import { discoverOpenAiModels, validateOpenAiCredential, } from "./credential-validation.js";
3
+ import { createRedactedOpenAiProviderConfigProjection, readProjectOpenAiProviderConfig, withProjectOpenAiProviderConfigLock, writeProjectOpenAiProviderConfig, } from "./provider-config.js";
4
+ function providerConfigSnapshot(options) {
5
+ const config = readProjectOpenAiProviderConfig(options.tuttiHome, options.projectId);
6
+ if (config.kind === "missing") {
7
+ return { kind: "unavailable", reason: "provider_not_configured" };
8
+ }
9
+ if (config.kind === "invalid") {
10
+ return { kind: "unavailable", reason: "provider_config_invalid" };
11
+ }
12
+ try {
13
+ const credential = readOpenAiApiKeyCredential(options.tuttiHome, options.projectId, config.config.credential_ref);
14
+ return { kind: "ready", config: config.config, apiKey: credential.secret };
15
+ }
16
+ catch {
17
+ return { kind: "unavailable", reason: "provider_credential_missing" };
18
+ }
19
+ }
20
+ function providerInput(config, apiKey) {
21
+ return {
22
+ apiKey,
23
+ ...(config.api_base_url === undefined ? {} : { apiBaseUrl: config.api_base_url }),
24
+ ...(config.organization_id === undefined ? {} : { organizationId: config.organization_id }),
25
+ ...(config.openai_project_id === undefined
26
+ ? {}
27
+ : { openAiProjectId: config.openai_project_id }),
28
+ };
29
+ }
30
+ export async function discoverProjectOpenAiModels(options) {
31
+ return await withProjectOpenAiProviderConfigLock(options, async () => {
32
+ const snapshot = providerConfigSnapshot(options);
33
+ if (snapshot.kind === "unavailable") {
34
+ return snapshot;
35
+ }
36
+ return await (options.discoverModels ?? discoverOpenAiModels)(providerInput(snapshot.config, snapshot.apiKey));
37
+ });
38
+ }
39
+ export async function updateProjectOpenAiDefaultModel(options) {
40
+ return await withProjectOpenAiProviderConfigLock(options, async () => {
41
+ const snapshot = providerConfigSnapshot(options);
42
+ if (snapshot.kind === "unavailable") {
43
+ return { kind: "provider_unavailable", reason: snapshot.reason };
44
+ }
45
+ if (snapshot.config.default_model !== options.expectedDefaultModel) {
46
+ return {
47
+ kind: "stale_default_model",
48
+ current_default_model: snapshot.config.default_model,
49
+ };
50
+ }
51
+ if (snapshot.config.default_model === options.defaultModel) {
52
+ return {
53
+ kind: "unchanged",
54
+ projection: createRedactedOpenAiProviderConfigProjection(snapshot.config),
55
+ };
56
+ }
57
+ const validator = options.validateCredential ??
58
+ ((input) => validateOpenAiCredential(input, options.now === undefined ? {} : { now: options.now }));
59
+ const validation = await validator({
60
+ ...providerInput(snapshot.config, snapshot.apiKey),
61
+ defaultModel: options.defaultModel,
62
+ });
63
+ if (validation.kind === "failed") {
64
+ return {
65
+ kind: "validation_failed",
66
+ reason: validation.reason,
67
+ retryable: validation.retryable,
68
+ };
69
+ }
70
+ const config = {
71
+ ...snapshot.config,
72
+ default_model: validation.default_model,
73
+ validated_at: validation.validated_at,
74
+ };
75
+ writeProjectOpenAiProviderConfig(options.tuttiHome, options.projectId, config);
76
+ return {
77
+ kind: "updated",
78
+ projection: createRedactedOpenAiProviderConfigProjection(config),
79
+ };
80
+ });
81
+ }
82
+ //# sourceMappingURL=provider-models.js.map
@@ -1,7 +1,7 @@
1
1
  import { createOpenAiCredentialRef, createRedactedOpenAiApiKeyLabel, isOpenAiApiKey, writeOpenAiApiKeyCredential, } from "./credential-store.js";
2
2
  import { validateOpenAiCredential, } from "./credential-validation.js";
3
3
  import { DEFAULT_OPENAI_MODEL, OPENAI_API_KEY_AUTH_KIND, OPENAI_PROVIDER } from "./model-config.js";
4
- import { createRedactedOpenAiProviderConfigProjection, writeProjectOpenAiProviderConfig, } from "./provider-config.js";
4
+ import { createRedactedOpenAiProviderConfigProjection, withProjectOpenAiProviderConfigLock, writeProjectOpenAiProviderConfig, } from "./provider-config.js";
5
5
  function trimOptional(value) {
6
6
  const trimmed = value?.trim();
7
7
  return trimmed === undefined || trimmed === "" ? undefined : trimmed;
@@ -32,7 +32,7 @@ function validationInput(options) {
32
32
  }
33
33
  return input;
34
34
  }
35
- export async function configureProjectOpenAiProvider(options) {
35
+ async function configureProjectOpenAiProviderUnlocked(options) {
36
36
  const apiKey = options.apiKey.trim();
37
37
  if (!isOpenAiApiKey(apiKey)) {
38
38
  return {
@@ -85,4 +85,7 @@ export async function configureProjectOpenAiProvider(options) {
85
85
  projection: createRedactedOpenAiProviderConfigProjection(config),
86
86
  };
87
87
  }
88
+ export async function configureProjectOpenAiProvider(options) {
89
+ return await withProjectOpenAiProviderConfigLock(options, () => configureProjectOpenAiProviderUnlocked(options));
90
+ }
88
91
  //# sourceMappingURL=provider-setup.js.map
@@ -1,4 +1,5 @@
1
1
  import { createActivityRef } from "@tutti/shared/ids";
2
+ import { safeRuntimeFailureSummary } from "../runtime-failures.js";
2
3
  export class RunEngine {
3
4
  active = null;
4
5
  lastConclusion = null;
@@ -43,7 +44,7 @@ export class RunEngine {
43
44
  catch (error) {
44
45
  this.finishActive(activityRef, {
45
46
  kind: "failed",
46
- summary: error instanceof Error ? error.message : "Run failed",
47
+ summary: safeRuntimeFailureSummary("Run", error, "run_failed"),
47
48
  }, options.onTransition);
48
49
  }
49
50
  };
@@ -251,13 +251,13 @@ export async function runArtifactCandidateGate(options) {
251
251
  try {
252
252
  applicability = await judgeApplicability(options.judge ?? createDefaultJudge(options), input);
253
253
  }
254
- catch (error) {
254
+ catch {
255
255
  return gateFailure({
256
256
  reasonCode: "artifact_applicability_failed",
257
257
  summary: "Tutti could not determine whether this candidate requires an Artifact.",
258
258
  guidance: "Retry when the Artifact applicability provider is available.",
259
259
  source: "applicability_judge",
260
- detail: error instanceof Error ? error.message : "Artifact applicability judge failed",
260
+ detail: "Artifact applicability could not be determined by the provider.",
261
261
  correctable: false,
262
262
  });
263
263
  }
@@ -1,5 +1,6 @@
1
1
  import type { ProjectId } from "@tutti/shared/ids";
2
2
  import type { ApprovalRequestManager } from "../approvals/index.js";
3
+ import { type CheckProcessCoordinator } from "../checks/index.js";
3
4
  import type { ControlPlaneLogger, RunPipelineResolution, RunPipelineRunner } from "../control-plane/index.js";
4
5
  import type { ProviderUsageRecorder } from "../provider-usage/index.js";
5
6
  import type { OpenAiStructuredOutputClient } from "../providers/openai/sdk-procedure-runner.js";
@@ -8,6 +9,8 @@ import { type ArtifactApplicabilityJudge } from "./artifact-applicability.js";
8
9
  import { type OpenAiProviderConfig } from "../providers/openai/provider-config.js";
9
10
  import { type CodexLinuxSandboxReadiness } from "../providers/openai/app-server/linux-sandbox-readiness.js";
10
11
  import type { CodexAppServerAgentContextRuntime } from "../providers/openai/app-server/skills.js";
12
+ import type { CodexAppServerInvocationCoordinator } from "../providers/openai/app-server/invocation-coordinator.js";
13
+ import type { CodexAppServerLifecycleIncident } from "../providers/openai/app-server/invocation-lifecycle.js";
11
14
  export { TASK_RUN_OUTPUT_SCHEMA } from "./task-run-output.js";
12
15
  export type OpenAiRunPipelineRunnerOptions = {
13
16
  config: OpenAiProviderConfig;
@@ -23,9 +26,12 @@ export type OpenAiRunPipelineRunnerOptions = {
23
26
  requestTimeoutMs?: number;
24
27
  turnTimeoutMs?: number;
25
28
  checkTimeoutMs?: number;
29
+ checkProcessCoordinator?: CheckProcessCoordinator;
26
30
  baseEnv?: NodeJS.ProcessEnv;
27
31
  logger?: ControlPlaneLogger | undefined;
28
32
  recordUsage?: ProviderUsageRecorder;
33
+ invocationCoordinator?: CodexAppServerInvocationCoordinator;
34
+ onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
29
35
  artifactApplicabilityClient?: OpenAiStructuredOutputClient;
30
36
  artifactApplicabilityJudge?: ArtifactApplicabilityJudge;
31
37
  validateArtifactCandidate?: (options: ValidateCandidateArtifactOptions) => Promise<CandidateArtifactValidationResult>;
@@ -40,6 +46,9 @@ export type ProjectOpenAiRunPipelineRunnerOptions = {
40
46
  approvalManager?: ApprovalRequestManager;
41
47
  logger?: ControlPlaneLogger | undefined;
42
48
  recordUsage?: ProviderUsageRecorder;
49
+ invocationCoordinator?: CodexAppServerInvocationCoordinator;
50
+ checkProcessCoordinator?: CheckProcessCoordinator;
51
+ onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
43
52
  };
44
53
  export declare function createOpenAiRunPipelineRunner(options: OpenAiRunPipelineRunnerOptions): RunPipelineRunner;
45
54
  export declare function resolveProjectOpenAiRunPipelineRunner(options: ProjectOpenAiRunPipelineRunnerOptions): RunPipelineResolution;
@@ -1,4 +1,4 @@
1
- import { createNodeNpmCheckPlan, executeCheckPlan } from "../checks/index.js";
1
+ import { createNodeNpmCheckPlan, executeCheckPlan, } from "../checks/index.js";
2
2
  import { runArtifactCandidateGate, } from "./artifact-applicability.js";
3
3
  import { checkSkipFields, checkStatus, checksSummary, completedWithoutRepoChanges, docsStatusForChangedPaths, failedBeforeChecks, failedFinishedResult, promotionNotAttempted, promotionNotPromoted, successfulRunSummary, } from "./result-projections.js";
4
4
  import { runWorkspaceWriteTask } from "./task-run-invocation.js";
@@ -13,28 +13,16 @@ function isRecord(value) {
13
13
  return typeof value === "object" && value !== null && !Array.isArray(value);
14
14
  }
15
15
  function summarizeProviderError(error) {
16
- const diagnosticParts = [
17
- error.details.turn_error_code === undefined
18
- ? ""
19
- : `provider_code=${error.details.turn_error_code}`,
20
- error.details.turn_error_http_status === undefined
21
- ? ""
22
- : `http_status=${error.details.turn_error_http_status}`,
23
- error.details.stderr_excerpt === undefined || error.details.stderr_excerpt.trim() === ""
24
- ? ""
25
- : `stderr=${error.details.stderr_excerpt}`,
26
- ].filter((part) => part !== "");
27
- const summary = diagnosticParts.length === 0
28
- ? error.message
29
- : `${error.message} (${diagnosticParts.join("; ")})`;
30
- const looksLikeSchemaError = summary.includes("invalid_json_schema") ||
31
- summary.includes("Invalid schema") ||
32
- summary.includes("response_format");
16
+ const looksLikeSchemaError = error.details.turn_error_code === "invalid_json_schema" ||
17
+ error.message.includes("invalid_json_schema") ||
18
+ error.message.includes("Invalid schema") ||
19
+ error.message.includes("response_format");
20
+ const errorCode = looksLikeSchemaError
21
+ ? "codex_app_server_output_schema_invalid"
22
+ : `codex_app_server_${error.code}`;
33
23
  return failedBeforeChecks({
34
- summary,
35
- errorCode: looksLikeSchemaError
36
- ? "codex_app_server_output_schema_invalid"
37
- : `codex_app_server_${error.code}`,
24
+ summary: `Provider execution failed before checks. (${errorCode})`,
25
+ errorCode,
38
26
  retryable: !looksLikeSchemaError &&
39
27
  (error.code === "app_server_protocol_error" ||
40
28
  error.code === "turn_timeout" ||
@@ -44,13 +32,13 @@ function summarizeProviderError(error) {
44
32
  }
45
33
  function summarizeWorkspaceOpsError(error, changedPaths) {
46
34
  return failedBeforeChecks({
47
- summary: error.message,
35
+ summary: `Workspace operation failed before checks. (workspace_ops_${error.code})`,
48
36
  errorCode: `workspace_ops_${error.code}`,
49
37
  retryable: error.code === "conflict",
50
38
  ...(changedPaths === undefined ? {} : { changedPaths }),
51
39
  promotion: error.code === "conflict"
52
- ? promotionNotPromoted("mainline_not_promotable", error.message)
53
- : promotionNotAttempted("workspace_ops_failed", error.message),
40
+ ? promotionNotPromoted("mainline_not_promotable", "Workspace state could not be accepted safely.")
41
+ : promotionNotAttempted("workspace_ops_failed", "Workspace operation failed before promotion."),
54
42
  });
55
43
  }
56
44
  function runLineageFields(input) {
@@ -359,6 +347,9 @@ async function runTaskAttempt(options, input) {
359
347
  workspaceRoot: options.workspaceRoot,
360
348
  plan: checkPlan,
361
349
  ...(options.checkTimeoutMs === undefined ? {} : { timeoutMs: options.checkTimeoutMs }),
350
+ ...(options.checkProcessCoordinator === undefined
351
+ ? {}
352
+ : { processCoordinator: options.checkProcessCoordinator }),
362
353
  }), (result) => ({ checks_status: result.status, command_count: result.commands.length }));
363
354
  if (checks.status === "failed") {
364
355
  const result = failedFinishedResult({
@@ -441,7 +432,7 @@ async function runTaskAttempt(options, input) {
441
432
  return summarizeWorkspaceOpsError(error, changedPaths);
442
433
  }
443
434
  return failedBeforeChecks({
444
- summary: error instanceof Error ? error.message : "Run pipeline failed",
435
+ summary: "Run pipeline failed before checks. (run_pipeline_failed)",
445
436
  errorCode: "run_pipeline_failed",
446
437
  retryable: false,
447
438
  ...(changedPaths === undefined ? {} : { changedPaths }),
@@ -529,6 +520,15 @@ export function resolveProjectOpenAiRunPipelineRunner(options) {
529
520
  ...(options.promptsRoot === undefined ? {} : { promptsRoot: options.promptsRoot }),
530
521
  ...(options.logger === undefined ? {} : { logger: options.logger }),
531
522
  ...(options.recordUsage === undefined ? {} : { recordUsage: options.recordUsage }),
523
+ ...(options.invocationCoordinator === undefined
524
+ ? {}
525
+ : { invocationCoordinator: options.invocationCoordinator }),
526
+ ...(options.checkProcessCoordinator === undefined
527
+ ? {}
528
+ : { checkProcessCoordinator: options.checkProcessCoordinator }),
529
+ ...(options.onLifecycleIncident === undefined
530
+ ? {}
531
+ : { onLifecycleIncident: options.onLifecycleIncident }),
532
532
  ...(process.platform === "linux"
533
533
  ? { codexSandboxReadiness: () => inspectCodexLinuxSandboxReadiness() }
534
534
  : {}),
@@ -111,6 +111,12 @@ export async function runWorkspaceWriteTask(options, input) {
111
111
  : { requestTimeoutMs: options.requestTimeoutMs }),
112
112
  ...(options.turnTimeoutMs === undefined ? {} : { turnTimeoutMs: options.turnTimeoutMs }),
113
113
  ...(options.baseEnv === undefined ? {} : { baseEnv: options.baseEnv }),
114
+ ...(options.invocationCoordinator === undefined
115
+ ? {}
116
+ : { invocationCoordinator: options.invocationCoordinator }),
117
+ ...(options.onLifecycleIncident === undefined
118
+ ? {}
119
+ : { onLifecycleIncident: options.onLifecycleIncident }),
114
120
  runtimeTelemetry: {
115
121
  activityRef: input.activity_ref,
116
122
  stageId: "provider_task_run",
@@ -0,0 +1,3 @@
1
+ export declare function stableRuntimeFailureCode(error: unknown, fallback: string): string;
2
+ export declare function safeRuntimeFailureSummary(subject: "Procedure" | "Run", error: unknown, fallbackCode: "procedure_failed" | "run_failed"): string;
3
+ //# sourceMappingURL=runtime-failures.d.ts.map
@@ -0,0 +1,22 @@
1
+ const STABLE_FAILURE_CODE = /^[a-z][a-z0-9_]{0,63}$/u;
2
+ export function stableRuntimeFailureCode(error, fallback) {
3
+ if (typeof error === "object" &&
4
+ error !== null &&
5
+ "code" in error &&
6
+ typeof error.code === "string" &&
7
+ STABLE_FAILURE_CODE.test(error.code)) {
8
+ return error.code;
9
+ }
10
+ return fallback;
11
+ }
12
+ export function safeRuntimeFailureSummary(subject, error, fallbackCode) {
13
+ const code = stableRuntimeFailureCode(error, fallbackCode);
14
+ if (code === "process_lifecycle_failed" || code === "process_tree_not_terminated") {
15
+ return `${subject} stopped because the provider process could not close safely. Restart the Host before retrying. (${code})`;
16
+ }
17
+ if (code === "admission_unavailable" || code === "admission_closed") {
18
+ return `${subject} stopped because provider execution is unavailable while the Host is closing. Retry after the Host is ready. (${code})`;
19
+ }
20
+ return `${subject} failed because the runtime returned an internal error. Retry the action. (${code})`;
21
+ }
22
+ //# sourceMappingURL=runtime-failures.js.map
@@ -393,7 +393,7 @@ export function formatLaunchLifecycleResult(result, options = {}) {
393
393
  }
394
394
  export async function waitForForegroundHostShutdown(handle, processLike = process) {
395
395
  const stop = () => {
396
- void handle.close();
396
+ void handle.close().catch(() => undefined);
397
397
  };
398
398
  processLike.once("SIGINT", stop);
399
399
  processLike.once("SIGTERM", stop);
@@ -0,0 +1,43 @@
1
+ type ControlPlaneLifecycleOwner = {
2
+ beginShutdown: () => void;
3
+ waitForIdle: () => Promise<void>;
4
+ };
5
+ type ChatAssistantLifecycleOwner = {
6
+ stopAccepting: () => void;
7
+ waitForIdle: () => Promise<void>;
8
+ };
9
+ type InvocationLifecycleOwner = {
10
+ stopAccepting: () => void;
11
+ closeActive: () => Promise<void>;
12
+ waitForIdle: () => Promise<void>;
13
+ };
14
+ type CheckLifecycleOwner = {
15
+ stopAccepting: () => void;
16
+ terminateActive: () => Promise<void>;
17
+ waitForIdle: () => Promise<void>;
18
+ };
19
+ type ApprovalLifecycleOwner = {
20
+ clearPending: () => void;
21
+ dispose: () => void;
22
+ };
23
+ export declare class HostProjectRuntimeShutdownError extends Error {
24
+ readonly code: "drain_timeout";
25
+ constructor(code: "drain_timeout", message: string);
26
+ }
27
+ export declare class HostProjectRuntimeLifecycle {
28
+ private readonly options;
29
+ private closePromise;
30
+ constructor(options: {
31
+ controlPlane: ControlPlaneLifecycleOwner;
32
+ chatAssistant: ChatAssistantLifecycleOwner;
33
+ invocationCoordinator: InvocationLifecycleOwner;
34
+ checkProcessCoordinator: CheckLifecycleOwner;
35
+ approvalManager: ApprovalLifecycleOwner;
36
+ gracefulDrainMs?: number;
37
+ forcedDrainMs?: number;
38
+ });
39
+ close(): Promise<void>;
40
+ private closeOnce;
41
+ }
42
+ export {};
43
+ //# sourceMappingURL=host-project-runtime-lifecycle.d.ts.map