@xfey/tutti 0.1.68 → 0.1.69

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.
@@ -43,7 +43,7 @@ export type ReadOnlyChatAssistantOptions = {
43
43
  workspaceRoot: string;
44
44
  model: ReadOnlyChatAssistantModel;
45
45
  promptsRoot?: string;
46
- onScratchpadSourceChanged?: () => void;
46
+ onScratchpadSourceChanged?: (message: MessageProjection) => void;
47
47
  now?: () => Date;
48
48
  };
49
49
  export declare function isChatAssistantOutput(value: unknown): value is ChatAssistantOutput;
@@ -206,7 +206,7 @@ export class ReadOnlyChatAssistant {
206
206
  invalidates: mainChatMessageInvalidates(),
207
207
  });
208
208
  if (refs?.scratchpad_source !== "exclude") {
209
- this.options.onScratchpadSourceChanged?.();
209
+ this.options.onScratchpadSourceChanged?.(message);
210
210
  }
211
211
  }
212
212
  appendClarificationRoundAssistantMessage(input) {
@@ -2,6 +2,16 @@ import { withHostStoreTransaction } from "../store/index.js";
2
2
  import { readScratchpadProjection } from "./scratchpad.js";
3
3
  import { readScratchpadSourceState } from "./scratchpad-source-state.js";
4
4
  import { nowIso, stringifyJson } from "./serialization.js";
5
+ function cursorIsAfter(left, right) {
6
+ if (left === undefined) {
7
+ return false;
8
+ }
9
+ if (right === undefined) {
10
+ return true;
11
+ }
12
+ const timeOrder = left.created_at.localeCompare(right.created_at);
13
+ return timeOrder > 0 || (timeOrder === 0 && left.message_id > right.message_id);
14
+ }
5
15
  function parseStringArray(value, field) {
6
16
  let parsed;
7
17
  try {
@@ -238,11 +248,18 @@ export function rollbackTaskCompileContext(db, input) {
238
248
  }
239
249
  const rolledBackAt = nowIso(input.now);
240
250
  const sourceState = readScratchpadSourceState(tx);
241
- const dirtySince = sourceState.in_flight !== undefined &&
242
- new Date(rolledBackAt).getTime() <= new Date(sourceState.in_flight.started_at).getTime()
243
- ? new Date(new Date(sourceState.in_flight.started_at).getTime() + 1).toISOString()
244
- : rolledBackAt;
245
251
  const after = context.source_window.after;
252
+ const through = context.source_window.through;
253
+ const hasBoundaryAfterSourceChanges = sourceState.dirty_since !== undefined ||
254
+ sourceState.in_flight !== undefined ||
255
+ cursorIsAfter(sourceState.refreshed_through_cursor, through);
256
+ const dirtySince = hasBoundaryAfterSourceChanges
257
+ ? sourceState.in_flight !== undefined &&
258
+ new Date(rolledBackAt).getTime() <= new Date(sourceState.in_flight.started_at).getTime()
259
+ ? new Date(new Date(sourceState.in_flight.started_at).getTime() + 1).toISOString()
260
+ : rolledBackAt
261
+ : null;
262
+ const restoredThrough = hasBoundaryAfterSourceChanges ? after : through;
246
263
  tx.prepare(`
247
264
  INSERT INTO scratchpad_source_state (
248
265
  id,
@@ -261,7 +278,7 @@ export function rollbackTaskCompileContext(db, input) {
261
278
  refreshed_through_cursor_message_id = excluded.refreshed_through_cursor_message_id,
262
279
  dirty_since = excluded.dirty_since,
263
280
  updated_at = excluded.updated_at
264
- `).run(after?.created_at ?? null, after?.message_id ?? null, after?.created_at ?? null, after?.message_id ?? null, dirtySince, rolledBackAt);
281
+ `).run(after?.created_at ?? null, after?.message_id ?? null, restoredThrough?.created_at ?? null, restoredThrough?.message_id ?? null, dirtySince, rolledBackAt);
265
282
  tx.prepare("DELETE FROM active_task_compile_context WHERE workflow_ref = ?").run(input.workflow_ref);
266
283
  return true;
267
284
  });
@@ -1,5 +1,5 @@
1
1
  import { type ActivityRef, type ClarificationRoundRef, type IdempotencyKey, type WorkflowInvocationRef } from "@tutti/shared/ids";
2
- import type { ExecutionStatusProjection, ContextSyncDisposition, ContextSyncResult, RefreshScratchpadDisposition, RefreshScratchpadResult, RunSchedulerNowDisposition, RunSchedulerNowPayload, RefreshReferenceSummariesDisposition, RefreshReferenceSummariesPayload, RefreshReferenceSummariesResult, SendClarificationRoundMessageDisposition, SendClarificationRoundMessagePayload, SendClarificationRoundMessageResult, SubmitClarificationRoundDisposition, SubmitClarificationRoundPayload, SubmitClarificationRoundResult, SubmitWorklistDisposition, SubmitWorklistPayload, SubmitWorklistResult, ProcedureLane } from "@tutti/shared/schemas/api";
2
+ import type { ExecutionStatusProjection, MessageProjection, ContextSyncDisposition, ContextSyncResult, RefreshScratchpadDisposition, RefreshScratchpadResult, RunSchedulerNowDisposition, RunSchedulerNowPayload, RefreshReferenceSummariesDisposition, RefreshReferenceSummariesPayload, RefreshReferenceSummariesResult, SendClarificationRoundMessageDisposition, SendClarificationRoundMessagePayload, SendClarificationRoundMessageResult, SubmitClarificationRoundDisposition, SubmitClarificationRoundPayload, SubmitClarificationRoundResult, SubmitWorklistDisposition, SubmitWorklistPayload, SubmitWorklistResult, ProcedureLane } from "@tutti/shared/schemas/api";
3
3
  import type { ContextSyncWorkflowTrigger } from "./workflows/index.js";
4
4
  import type { ControlPlaneCommandResult, Phase5ControlPlaneOptions, StartupRecoveryOptions, StartupRecoveryResult, TrustedHumanAuthor } from "./types.js";
5
5
  import { type ProjectBriefRefreshReason, type RefreshProjectBriefProjectionResult } from "./project-brief-refresh.js";
@@ -30,7 +30,7 @@ export declare class Phase5ControlPlane {
30
30
  startScratchpadRefresh(options?: {
31
31
  lane?: ProcedureLane;
32
32
  }): ControlPlaneCommandResult<RefreshScratchpadDisposition, RefreshScratchpadResult>;
33
- notifyScratchpadSourceChanged(): void;
33
+ notifyScratchpadSourceChanged(message: MessageProjection): void;
34
34
  refreshProjectBrief(options: {
35
35
  reason: ProjectBriefRefreshReason;
36
36
  workflowRef?: WorkflowInvocationRef;
@@ -9,6 +9,7 @@ import { runControlPlaneStartupRecovery } from "./startup-recovery.js";
9
9
  import { readControlPlaneExecutionStatus } from "./execution-status.js";
10
10
  import { ScratchpadAutoRefreshScheduler } from "./scratchpad-auto-refresh.js";
11
11
  import { startControlPlaneScratchpadRefresh } from "./scratchpad-refresh-start.js";
12
+ import { shouldUseMessageAsScratchpadSource } from "./scratchpad-source-messages.js";
12
13
  import { startControlPlaneTaskCompile } from "./task-compile-start.js";
13
14
  import { startControlPlaneTaskCompileContinuation } from "./task-compile-continuation.js";
14
15
  import { startControlPlaneTaskBoundFollowUpCheck } from "./follow-up-start.js";
@@ -106,7 +107,10 @@ export class Phase5ControlPlane {
106
107
  publishProcedureTransition: (state) => this.publishProcedureTransition(state),
107
108
  });
108
109
  }
109
- notifyScratchpadSourceChanged() {
110
+ notifyScratchpadSourceChanged(message) {
111
+ if (!shouldUseMessageAsScratchpadSource(message)) {
112
+ return;
113
+ }
110
114
  this.scratchpadAutoRefreshScheduler.notifySourceChanged();
111
115
  }
112
116
  refreshProjectBrief(options) {
@@ -1,5 +1,6 @@
1
1
  import { StringDecoder } from "node:string_decoder";
2
2
  import { redactText } from "@tutti/shared/utils";
3
+ const PROCESS_EXIT_STDERR_EXCERPT_MAX_LENGTH = 500;
3
4
  export class CodexAppServerProtocolError extends Error {
4
5
  code;
5
6
  constructor(message, code) {
@@ -21,6 +22,16 @@ function normalizeProtocolError(method, error) {
21
22
  const message = typeof rawMessage === "string" ? redactText(rawMessage) : "protocol_error";
22
23
  return new CodexAppServerProtocolError(`Codex app-server request failed: ${method}: ${code}: ${message}`, "request_failed");
23
24
  }
25
+ function processExitStderrExcerpt(value) {
26
+ const normalized = redactText(value).replace(/\s+/gu, " ").trim();
27
+ if (normalized.length === 0) {
28
+ return "";
29
+ }
30
+ if (normalized.length <= PROCESS_EXIT_STDERR_EXCERPT_MAX_LENGTH) {
31
+ return normalized;
32
+ }
33
+ return `${normalized.slice(0, PROCESS_EXIT_STDERR_EXCERPT_MAX_LENGTH)}...`;
34
+ }
24
35
  export class CodexAppServerJsonRpcClient {
25
36
  child;
26
37
  options;
@@ -47,7 +58,8 @@ export class CodexAppServerJsonRpcClient {
47
58
  });
48
59
  child.on("exit", (code, signal) => {
49
60
  this.closed = true;
50
- this.rejectAll(new CodexAppServerProtocolError(`Codex app-server exited before request completed: code=${code ?? "null"} signal=${signal ?? "null"}`, "process_exited"));
61
+ const stderrExcerpt = processExitStderrExcerpt(this.stderrText);
62
+ this.rejectAll(new CodexAppServerProtocolError(`Codex app-server exited before request completed: code=${code ?? "null"} signal=${signal ?? "null"}${stderrExcerpt === "" ? "" : ` stderr=${stderrExcerpt}`}`, "process_exited"));
51
63
  });
52
64
  child.on("error", (error) => {
53
65
  this.closed = true;
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { CODEX_APP_SERVER_COMMAND } from "../codex-app-server.js";
5
+ import { buildCodexCliProcessPlan } from "../codex-app-server.js";
6
6
  import { createCodexSpawnEnv } from "./runtime-helpers.js";
7
7
  const DEFAULT_PROBE_TIMEOUT_MS = 10_000;
8
8
  export const CODEX_LINUX_SANDBOX_GUIDANCE = "Install bubblewrap. On Ubuntu 24.04, also install apparmor-profiles and apparmor-utils and load the distribution bwrap-userns-restrict profile. Then run `tutti doctor` again.";
@@ -74,9 +74,12 @@ export function inspectCodexLinuxSandboxReadiness(options = {}) {
74
74
  const codexHome = join(tempRoot, "codex-home");
75
75
  mkdirSync(codexHome, { recursive: true, mode: 0o700 });
76
76
  const runProbe = options.runProbe ?? defaultProbeRunner;
77
- return classifyProbeResult(runProbe({
78
- command: options.codexCommand ?? CODEX_APP_SERVER_COMMAND,
77
+ const processPlan = buildCodexCliProcessPlan({
79
78
  args: ["sandbox", "--", "/bin/true"],
79
+ ...(options.codexCommand === undefined ? {} : { codexCommand: options.codexCommand }),
80
+ });
81
+ return classifyProbeResult(runProbe({
82
+ ...processPlan,
80
83
  cwd: tempRoot,
81
84
  env: createCodexSpawnEnv(codexHome, options.baseEnv ?? process.env),
82
85
  timeoutMs: options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS,
@@ -120,9 +120,9 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
120
120
  const processPlan = buildCodexAppServerProcessPlan({
121
121
  codexHome,
122
122
  configOverrides: buildCodexAppServerProviderConfigOverrides(options),
123
+ ...(options.codexCommand === undefined ? {} : { codexCommand: options.codexCommand }),
123
124
  });
124
- const command = options.codexCommand ?? processPlan.command;
125
- const child = spawn(command, processPlan.args, {
125
+ const child = spawn(processPlan.command, processPlan.args, {
126
126
  cwd: process.cwd(),
127
127
  env: createCodexSpawnEnv(codexHome, options.baseEnv ?? process.env),
128
128
  stdio: "pipe",
@@ -25,9 +25,9 @@ export async function runCodexAppServerNoWriteSmoke(options) {
25
25
  const processPlan = buildCodexAppServerProcessPlan({
26
26
  codexHome,
27
27
  configOverrides: buildCodexAppServerProviderConfigOverrides(options),
28
+ ...(options.codexCommand === undefined ? {} : { codexCommand: options.codexCommand }),
28
29
  });
29
- const command = options.codexCommand ?? processPlan.command;
30
- const child = spawn(command, processPlan.args, {
30
+ const child = spawn(processPlan.command, processPlan.args, {
31
31
  cwd: process.cwd(),
32
32
  env: createCodexSpawnEnv(codexHome, options.baseEnv ?? process.env),
33
33
  stdio: "pipe",
@@ -196,9 +196,9 @@ export async function runCodexAppServerWorkspaceWriteRun(options) {
196
196
  const processPlan = buildCodexAppServerProcessPlan({
197
197
  codexHome,
198
198
  configOverrides: buildCodexAppServerProviderConfigOverrides(options),
199
+ ...(options.codexCommand === undefined ? {} : { codexCommand: options.codexCommand }),
199
200
  });
200
- const command = options.codexCommand ?? processPlan.command;
201
- const child = spawn(command, processPlan.args, {
201
+ const child = spawn(processPlan.command, processPlan.args, {
202
202
  cwd: process.cwd(),
203
203
  env: createCodexSpawnEnv(codexHome, options.baseEnv ?? process.env),
204
204
  stdio: "pipe",
@@ -1,3 +1,4 @@
1
+ export declare const CODEX_APP_SERVER_ENTRYPOINT: string;
1
2
  export declare const CODEX_APP_SERVER_COMMAND: string;
2
3
  export declare const CODEX_APP_SERVER_LISTEN_URL: "stdio://";
3
4
  export declare const CODEX_APP_SERVER_CLIENT_NAME: "tutti_host";
@@ -73,8 +74,13 @@ export type CodexAppServerTurnStartTemplatePlan = {
73
74
  };
74
75
  export type BuildCodexAppServerProcessPlanOptions = {
75
76
  codexHome: string;
77
+ codexCommand?: string;
76
78
  configOverrides?: readonly CodexAppServerConfigOverride[];
77
79
  };
80
+ export type CodexCliProcessPlan = {
81
+ command: string;
82
+ args: string[];
83
+ };
78
84
  export type CodexAppServerNoWriteSmokePlan = {
79
85
  process: CodexAppServerProcessPlan;
80
86
  initialize: CodexAppServerRequestPlan<"initialize", CodexAppServerInitializeRequestParams>;
@@ -107,6 +113,10 @@ export declare const CODEX_NO_WRITE_SMOKE_OUTPUT_SCHEMA: {
107
113
  export declare function createCodexAppServerProcessEnv(options: {
108
114
  codexHome: string;
109
115
  }): Record<string, string>;
116
+ export declare function buildCodexCliProcessPlan(options: {
117
+ args: readonly string[];
118
+ codexCommand?: string;
119
+ }): CodexCliProcessPlan;
110
120
  export declare function buildCodexAppServerProcessPlan(options: BuildCodexAppServerProcessPlanOptions): CodexAppServerProcessPlan;
111
121
  export declare function createCodexAppServerNoWriteSmokePlan(options: CodexNoWriteSmokePlanOptions): CodexAppServerNoWriteSmokePlan;
112
122
  export declare class CodexAppServerJsonlParseError extends Error {
@@ -2,7 +2,8 @@ import { createRequire } from "node:module";
2
2
  import { dirname, isAbsolute, join } from "node:path";
3
3
  import { DEFAULT_OPENAI_MODEL } from "./model-config.js";
4
4
  const require = createRequire(import.meta.url);
5
- export const CODEX_APP_SERVER_COMMAND = join(dirname(require.resolve("@openai/codex/package.json")), "bin", "codex.js");
5
+ export const CODEX_APP_SERVER_ENTRYPOINT = join(dirname(require.resolve("@openai/codex/package.json")), "bin", "codex.js");
6
+ export const CODEX_APP_SERVER_COMMAND = process.execPath;
6
7
  export const CODEX_APP_SERVER_LISTEN_URL = "stdio://";
7
8
  export const CODEX_APP_SERVER_CLIENT_NAME = "tutti_host";
8
9
  export const CODEX_APP_SERVER_CLIENT_TITLE = "Tutti Host";
@@ -37,12 +38,28 @@ export function createCodexAppServerProcessEnv(options) {
37
38
  CODEX_HOME: options.codexHome,
38
39
  };
39
40
  }
40
- export function buildCodexAppServerProcessPlan(options) {
41
- const args = ["app-server", "--listen", CODEX_APP_SERVER_LISTEN_URL];
42
- pushConfigOverrides(args, options.configOverrides ?? []);
41
+ export function buildCodexCliProcessPlan(options) {
42
+ if (options.codexCommand !== undefined) {
43
+ assertNonEmpty(options.codexCommand, "codexCommand");
44
+ return {
45
+ command: options.codexCommand,
46
+ args: [...options.args],
47
+ };
48
+ }
43
49
  return {
44
50
  command: CODEX_APP_SERVER_COMMAND,
45
- args,
51
+ args: [CODEX_APP_SERVER_ENTRYPOINT, ...options.args],
52
+ };
53
+ }
54
+ export function buildCodexAppServerProcessPlan(options) {
55
+ const codexArgs = ["app-server", "--listen", CODEX_APP_SERVER_LISTEN_URL];
56
+ pushConfigOverrides(codexArgs, options.configOverrides ?? []);
57
+ const processPlan = buildCodexCliProcessPlan({
58
+ args: codexArgs,
59
+ ...(options.codexCommand === undefined ? {} : { codexCommand: options.codexCommand }),
60
+ });
61
+ return {
62
+ ...processPlan,
46
63
  env: createCodexAppServerProcessEnv({ codexHome: options.codexHome }),
47
64
  transport: {
48
65
  kind: "stdio",
@@ -283,7 +283,7 @@ export async function startForegroundHostServer(options) {
283
283
  return await resolution.model.answer(input);
284
284
  },
285
285
  },
286
- onScratchpadSourceChanged: () => controlPlane.notifyScratchpadSourceChanged(),
286
+ onScratchpadSourceChanged: (message) => controlPlane.notifyScratchpadSourceChanged(message),
287
287
  now,
288
288
  });
289
289
  let close = async () => { };
@@ -71,7 +71,7 @@ export function registerMessagesRoutes(app, { options, workspaceEvents, }) {
71
71
  },
72
72
  invalidates: mainChatMessageInvalidates(),
73
73
  });
74
- options.controlPlane?.notifyScratchpadSourceChanged();
74
+ options.controlPlane?.notifyScratchpadSourceChanged(execution.result.message);
75
75
  options.chatAssistant?.enqueue({ message: execution.result.message });
76
76
  }
77
77
  return response;
@@ -137,7 +137,7 @@ export function registerReferenceFilesRoutes(app, { options, workspaceEvents, })
137
137
  },
138
138
  invalidates,
139
139
  });
140
- options.controlPlane?.notifyScratchpadSourceChanged();
140
+ options.controlPlane?.notifyScratchpadSourceChanged(execution.result.message);
141
141
  options.controlPlane?.startReferenceSummaryRefresh({
142
142
  paths: [execution.result.file.path],
143
143
  });
@@ -19,10 +19,12 @@ export declare function createDesktopLocalConsoleInvocationContext(options: {
19
19
  export declare function createLocalConsoleServiceEnvironment(options: {
20
20
  env: NodeJS.ProcessEnv;
21
21
  tuttiHome: string;
22
+ runtimeExecutablePath?: string;
22
23
  }): NodeJS.ProcessEnv;
23
24
  export declare function createLocalConsoleOperationEnvironment(options: {
24
25
  serviceEnvironment: NodeJS.ProcessEnv;
25
26
  invocationEnvironment: LocalConsoleInvocationEnvironment;
26
27
  tuttiHome: string;
28
+ runtimeExecutablePath?: string;
27
29
  }): NodeJS.ProcessEnv;
28
30
  //# sourceMappingURL=invocation-context.d.ts.map
@@ -1,4 +1,4 @@
1
- import { resolve } from "node:path";
1
+ import { delimiter, dirname, resolve } from "node:path";
2
2
  export const LOCAL_CONSOLE_INVOCATION_ENV_KEYS = [
3
3
  "DBUS_SESSION_BUS_ADDRESS",
4
4
  "DISPLAY",
@@ -12,6 +12,12 @@ export const LOCAL_CONSOLE_INVOCATION_ENV_KEYS = [
12
12
  "WAYLAND_DISPLAY",
13
13
  "XDG_CURRENT_DESKTOP",
14
14
  ];
15
+ function mergeExecutablePaths(values, runtimeExecutablePath) {
16
+ const entries = [dirname(runtimeExecutablePath), ...values.flatMap((value) => value?.split(delimiter) ?? [])]
17
+ .map((entry) => entry.trim())
18
+ .filter((entry) => entry !== "");
19
+ return [...new Set(entries)].join(delimiter);
20
+ }
15
21
  export function collectLocalConsoleInvocationEnvironment(env) {
16
22
  const environment = {};
17
23
  for (const key of LOCAL_CONSOLE_INVOCATION_ENV_KEYS) {
@@ -52,12 +58,15 @@ export function createLocalConsoleServiceEnvironment(options) {
52
58
  environment[key] = value;
53
59
  }
54
60
  }
61
+ environment.PATH = mergeExecutablePaths([environment.PATH], options.runtimeExecutablePath ?? process.execPath);
55
62
  return environment;
56
63
  }
57
64
  export function createLocalConsoleOperationEnvironment(options) {
65
+ const executablePath = mergeExecutablePaths([options.invocationEnvironment.PATH, options.serviceEnvironment.PATH], options.runtimeExecutablePath ?? process.execPath);
58
66
  return {
59
67
  ...options.serviceEnvironment,
60
68
  ...options.invocationEnvironment,
69
+ PATH: executablePath,
61
70
  TUTTI_HOME: options.tuttiHome,
62
71
  };
63
72
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfey/tutti",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",