@renseiai/agentfactory 0.8.21 → 0.8.23

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 (27) hide show
  1. package/dist/src/config/repository-config.d.ts +3 -3
  2. package/dist/src/orchestrator/index.d.ts +1 -0
  3. package/dist/src/orchestrator/index.d.ts.map +1 -1
  4. package/dist/src/orchestrator/index.js +2 -0
  5. package/dist/src/orchestrator/null-issue-tracker-client.d.ts +34 -0
  6. package/dist/src/orchestrator/null-issue-tracker-client.d.ts.map +1 -0
  7. package/dist/src/orchestrator/null-issue-tracker-client.js +72 -0
  8. package/dist/src/orchestrator/orchestrator.d.ts +19 -0
  9. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  10. package/dist/src/orchestrator/orchestrator.js +134 -15
  11. package/dist/src/orchestrator/state-types.d.ts +3 -0
  12. package/dist/src/orchestrator/state-types.d.ts.map +1 -1
  13. package/dist/src/providers/codex-app-server-provider.d.ts +87 -0
  14. package/dist/src/providers/codex-app-server-provider.d.ts.map +1 -1
  15. package/dist/src/providers/codex-app-server-provider.integration.test.d.ts +14 -0
  16. package/dist/src/providers/codex-app-server-provider.integration.test.d.ts.map +1 -0
  17. package/dist/src/providers/codex-app-server-provider.integration.test.js +909 -0
  18. package/dist/src/providers/codex-app-server-provider.js +339 -52
  19. package/dist/src/providers/codex-app-server-provider.test.js +838 -10
  20. package/dist/src/providers/codex-provider.d.ts +2 -0
  21. package/dist/src/providers/codex-provider.d.ts.map +1 -1
  22. package/dist/src/providers/codex-provider.js +36 -6
  23. package/dist/src/providers/codex-provider.test.js +12 -3
  24. package/dist/src/providers/types.d.ts +17 -0
  25. package/dist/src/providers/types.d.ts.map +1 -1
  26. package/dist/src/workflow/workflow-types.d.ts +5 -5
  27. package/package.json +2 -2
@@ -23,14 +23,56 @@
23
23
  */
24
24
  import { spawn } from 'child_process';
25
25
  import { createInterface } from 'readline';
26
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
27
+ import { join } from 'path';
28
+ import { homedir } from 'os';
26
29
  import { classifyTool } from '../tools/tool-category.js';
27
30
  import { evaluateCommandApproval, evaluateFileChangeApproval, } from './codex-approval-bridge.js';
31
+ function isServerRequest(msg) {
32
+ return 'id' in msg && 'method' in msg;
33
+ }
28
34
  function isResponse(msg) {
29
- return 'id' in msg && typeof msg.id === 'number';
35
+ return 'id' in msg && !('method' in msg);
30
36
  }
31
37
  function isNotification(msg) {
32
38
  return 'method' in msg && !('id' in msg);
33
39
  }
40
+ // ---------------------------------------------------------------------------
41
+ // Codex model mapping (SUP-1749)
42
+ // ---------------------------------------------------------------------------
43
+ export const CODEX_MODEL_MAP = {
44
+ 'opus': 'gpt-5-codex',
45
+ 'sonnet': 'gpt-5.2-codex',
46
+ 'haiku': 'gpt-5.3-codex',
47
+ };
48
+ export const CODEX_DEFAULT_MODEL = 'gpt-5-codex';
49
+ export function resolveCodexModel(config) {
50
+ if (config.model)
51
+ return config.model;
52
+ const tier = config.env.CODEX_MODEL_TIER;
53
+ if (tier && CODEX_MODEL_MAP[tier])
54
+ return CODEX_MODEL_MAP[tier];
55
+ if (config.env.CODEX_MODEL)
56
+ return config.env.CODEX_MODEL;
57
+ return CODEX_DEFAULT_MODEL;
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // Codex pricing and cost calculation (SUP-1750)
61
+ // ---------------------------------------------------------------------------
62
+ /** Codex pricing per 1M tokens (USD). Update when pricing changes. */
63
+ export const CODEX_PRICING = {
64
+ 'gpt-5-codex': { input: 2.00, cachedInput: 0.50, output: 8.00 },
65
+ 'gpt-5.2-codex': { input: 1.00, cachedInput: 0.25, output: 4.00 },
66
+ 'gpt-5.3-codex': { input: 0.50, cachedInput: 0.125, output: 2.00 },
67
+ };
68
+ export const CODEX_DEFAULT_PRICING = CODEX_PRICING['gpt-5-codex'];
69
+ export function calculateCostUsd(inputTokens, cachedInputTokens, outputTokens, model) {
70
+ const pricing = (model && CODEX_PRICING[model]) || CODEX_DEFAULT_PRICING;
71
+ const freshInputTokens = Math.max(0, inputTokens - cachedInputTokens);
72
+ return ((freshInputTokens / 1_000_000) * pricing.input +
73
+ (cachedInputTokens / 1_000_000) * pricing.cachedInput +
74
+ (outputTokens / 1_000_000) * pricing.output);
75
+ }
34
76
  /**
35
77
  * Manages a single long-lived `codex app-server` process.
36
78
  *
@@ -60,19 +102,109 @@ export class AppServerProcessManager {
60
102
  this.cwd = options.cwd;
61
103
  this.env = options.env || {};
62
104
  }
105
+ /**
106
+ * Get the PID file path for tracking the app-server process.
107
+ * Used for orphan detection on startup.
108
+ */
109
+ static getPidFilePath() {
110
+ const dir = join(homedir(), '.agentfactory');
111
+ if (!existsSync(dir)) {
112
+ mkdirSync(dir, { recursive: true });
113
+ }
114
+ return join(dir, 'codex-app-server.pid');
115
+ }
116
+ /**
117
+ * Kill any orphaned app-server process from a prior fleet run.
118
+ * Called before starting a new process to prevent resource leaks.
119
+ */
120
+ static killOrphanedProcess() {
121
+ const pidFile = AppServerProcessManager.getPidFilePath();
122
+ if (!existsSync(pidFile))
123
+ return;
124
+ try {
125
+ const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
126
+ if (isNaN(pid)) {
127
+ unlinkSync(pidFile);
128
+ return;
129
+ }
130
+ // Check if the process is alive
131
+ try {
132
+ process.kill(pid, 0); // signal 0 = check existence
133
+ }
134
+ catch {
135
+ // Process is dead, just clean up the PID file
136
+ unlinkSync(pidFile);
137
+ return;
138
+ }
139
+ // Process is alive — kill it
140
+ console.error(`[CodexAppServer] Killing orphaned app-server process (PID ${pid})`);
141
+ try {
142
+ process.kill(pid, 'SIGTERM');
143
+ // Give it a moment, then force kill
144
+ setTimeout(() => {
145
+ try {
146
+ process.kill(pid, 0); // still alive?
147
+ process.kill(pid, 'SIGKILL');
148
+ }
149
+ catch {
150
+ // Already dead
151
+ }
152
+ }, 2000);
153
+ }
154
+ catch {
155
+ // Kill failed, process may have already exited
156
+ }
157
+ unlinkSync(pidFile);
158
+ }
159
+ catch {
160
+ // PID file read/delete failed — ignore
161
+ }
162
+ }
163
+ /**
164
+ * Write the current app-server PID to the PID file.
165
+ */
166
+ writePidFile() {
167
+ if (!this.process?.pid)
168
+ return;
169
+ try {
170
+ writeFileSync(AppServerProcessManager.getPidFilePath(), String(this.process.pid));
171
+ }
172
+ catch {
173
+ // Best effort
174
+ }
175
+ }
176
+ /**
177
+ * Remove the PID file on shutdown.
178
+ */
179
+ static removePidFile() {
180
+ try {
181
+ const pidFile = AppServerProcessManager.getPidFilePath();
182
+ if (existsSync(pidFile)) {
183
+ unlinkSync(pidFile);
184
+ }
185
+ }
186
+ catch {
187
+ // Best effort
188
+ }
189
+ }
63
190
  /**
64
191
  * Start the app-server process and complete the initialization handshake.
65
192
  * Idempotent — returns immediately if already initialized.
193
+ * Kills any orphaned app-server from a prior fleet run before starting.
66
194
  */
67
195
  async start() {
68
196
  if (this.initialized && this.process && !this.process.killed) {
69
197
  return;
70
198
  }
199
+ // Kill orphaned app-server from prior fleet run
200
+ AppServerProcessManager.killOrphanedProcess();
71
201
  this.process = spawn(this.codexBin, ['app-server'], {
72
202
  cwd: this.cwd,
73
203
  env: { ...process.env, ...this.env },
74
204
  stdio: ['pipe', 'pipe', 'pipe'],
75
205
  });
206
+ // Track PID for orphan detection on next startup
207
+ this.writePidFile();
76
208
  this.readline = createInterface({ input: this.process.stdout });
77
209
  // Parse incoming JSONL messages
78
210
  this.readline.on('line', (line) => {
@@ -87,7 +219,12 @@ export class AppServerProcessManager {
87
219
  // Non-JSON output — ignore
88
220
  return;
89
221
  }
90
- if (isResponse(msg)) {
222
+ if (isServerRequest(msg)) {
223
+ // Server requests have both `id` and `method` — Codex expects a response.
224
+ // Approval requests (commandExecution/requestApproval, etc.) come as server requests.
225
+ this.handleServerRequest(msg);
226
+ }
227
+ else if (isResponse(msg)) {
91
228
  this.handleResponse(msg);
92
229
  }
93
230
  else if (isNotification(msg)) {
@@ -106,6 +243,16 @@ export class AppServerProcessManager {
106
243
  });
107
244
  // Perform initialization handshake
108
245
  await this.initialize();
246
+ // Discover available models (best effort — older servers may not support model/list)
247
+ try {
248
+ const models = await this.listModels();
249
+ if (models.length > 0) {
250
+ console.error(`[CodexAppServer] Available models: ${models.map(m => m.id).join(', ')}`);
251
+ }
252
+ }
253
+ catch {
254
+ console.error('[CodexAppServer] model/list not supported by this server version');
255
+ }
109
256
  }
110
257
  /**
111
258
  * JSON-RPC 2.0 initialization handshake:
@@ -174,6 +321,44 @@ export class AppServerProcessManager {
174
321
  pending.resolve(response.result);
175
322
  }
176
323
  }
324
+ /**
325
+ * Handle an incoming JSON-RPC server request (has both `id` and `method`).
326
+ * Codex sends approval requests as server requests that expect a response.
327
+ * Route to the thread listener (as a notification-like object) and store
328
+ * the request ID so the handle can respond.
329
+ */
330
+ handleServerRequest(request) {
331
+ const threadId = request.params?.threadId;
332
+ console.error(`[CodexAppServer] Server request: ${request.method} (id=${request.id}, thread=${threadId ?? 'none'})`);
333
+ // Wrap as a notification-compatible object for the thread listener,
334
+ // but include the id so the handle can respond.
335
+ const notificationLike = {
336
+ method: request.method,
337
+ params: { ...request.params, _serverRequestId: request.id },
338
+ };
339
+ if (threadId) {
340
+ const listener = this.threadListeners.get(threadId);
341
+ if (listener) {
342
+ listener(notificationLike);
343
+ return;
344
+ }
345
+ }
346
+ // No thread listener — auto-accept to avoid hanging
347
+ console.error(`[CodexAppServer] No thread listener for ${request.method} — auto-accepting`);
348
+ this.respondToServerRequest(request.id, { decision: 'acceptForSession' });
349
+ }
350
+ /**
351
+ * Send a JSON-RPC response to a server request.
352
+ */
353
+ respondToServerRequest(requestId, result) {
354
+ if (!this.process?.stdin?.writable) {
355
+ console.error('[CodexAppServer] Cannot respond to server request: stdin not writable');
356
+ return;
357
+ }
358
+ console.error(`[CodexAppServer] Responding to server request ${requestId}: ${JSON.stringify(result)}`);
359
+ const response = JSON.stringify({ jsonrpc: '2.0', id: requestId, result });
360
+ this.process.stdin.write(response + '\n');
361
+ }
177
362
  /**
178
363
  * Handle an incoming JSON-RPC notification.
179
364
  * Routes to the appropriate thread listener based on threadId in params.
@@ -236,8 +421,8 @@ export class AppServerProcessManager {
236
421
  }
237
422
  try {
238
423
  await this.request('config/batchWrite', {
239
- entries: [
240
- { key: 'mcpServers', value: mcpServers },
424
+ edits: [
425
+ { keyPath: 'mcpServers', mergeStrategy: 'replace', value: mcpServers },
241
426
  ],
242
427
  });
243
428
  this.mcpConfigured = true;
@@ -264,6 +449,13 @@ export class AppServerProcessManager {
264
449
  return [];
265
450
  }
266
451
  }
452
+ /**
453
+ * Discover available models from the app-server via model/list.
454
+ */
455
+ async listModels() {
456
+ const result = await this.request('model/list', {});
457
+ return result?.models ?? [];
458
+ }
267
459
  /**
268
460
  * Get the PID of the app-server process.
269
461
  */
@@ -281,6 +473,8 @@ export class AppServerProcessManager {
281
473
  }
282
474
  async performShutdown() {
283
475
  this.initialized = false;
476
+ // Remove PID file before killing process
477
+ AppServerProcessManager.removePidFile();
284
478
  // Clear all pending requests
285
479
  this.rejectAllPending(new Error('App server shutting down'));
286
480
  // Kill the process
@@ -359,6 +553,7 @@ export function mapAppServerNotification(notification, state) {
359
553
  if (turn?.usage) {
360
554
  state.totalInputTokens += turn.usage.input_tokens ?? 0;
361
555
  state.totalOutputTokens += turn.usage.output_tokens ?? 0;
556
+ state.totalCachedInputTokens += turn.usage.cached_input_tokens ?? 0;
362
557
  }
363
558
  if (turnStatus === 'completed') {
364
559
  return [{
@@ -367,6 +562,8 @@ export function mapAppServerNotification(notification, state) {
367
562
  cost: {
368
563
  inputTokens: state.totalInputTokens || undefined,
369
564
  outputTokens: state.totalOutputTokens || undefined,
565
+ cachedInputTokens: state.totalCachedInputTokens || undefined,
566
+ totalCostUsd: calculateCostUsd(state.totalInputTokens, state.totalCachedInputTokens, state.totalOutputTokens, state.model ?? undefined),
370
567
  numTurns: state.turnCount || undefined,
371
568
  },
372
569
  raw: notification,
@@ -381,6 +578,8 @@ export function mapAppServerNotification(notification, state) {
381
578
  cost: {
382
579
  inputTokens: state.totalInputTokens || undefined,
383
580
  outputTokens: state.totalOutputTokens || undefined,
581
+ cachedInputTokens: state.totalCachedInputTokens || undefined,
582
+ totalCostUsd: calculateCostUsd(state.totalInputTokens, state.totalCachedInputTokens, state.totalOutputTokens, state.model ?? undefined),
384
583
  numTurns: state.turnCount || undefined,
385
584
  },
386
585
  raw: notification,
@@ -408,7 +607,7 @@ export function mapAppServerNotification(notification, state) {
408
607
  return mapAppServerItemEvent(method, params);
409
608
  // --- Item deltas (streaming) ---
410
609
  case 'item/agentMessage/delta': {
411
- const text = params.text;
610
+ const text = (params.delta ?? params.text);
412
611
  if (text) {
413
612
  return [{
414
613
  type: 'assistant_text',
@@ -435,7 +634,7 @@ export function mapAppServerNotification(notification, state) {
435
634
  return [{
436
635
  type: 'system',
437
636
  subtype: 'command_progress',
438
- message: (params.delta ?? params.output) ?? '',
637
+ message: stripAnsi((params.delta ?? params.output) ?? ''),
439
638
  raw: notification,
440
639
  }];
441
640
  // --- Turn diff/plan ---
@@ -462,6 +661,16 @@ export function mapAppServerNotification(notification, state) {
462
661
  }];
463
662
  }
464
663
  }
664
+ /**
665
+ * Strip ANSI escape codes from text.
666
+ * Codex shell commands produce raw terminal output with color codes,
667
+ * cursor movement, etc. that pollute logs and activity tracking.
668
+ */
669
+ // eslint-disable-next-line no-control-regex
670
+ const ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b[()][AB012]|\x1b\[[\d;]*m/g;
671
+ function stripAnsi(text) {
672
+ return text.replace(ANSI_PATTERN, '');
673
+ }
465
674
  /**
466
675
  * Map item/started and item/completed notifications to AgentEvents.
467
676
  * Exported for unit testing.
@@ -507,7 +716,7 @@ export function mapAppServerItemEvent(method, params) {
507
716
  type: 'tool_result',
508
717
  toolName: 'shell',
509
718
  toolUseId: item.id,
510
- content: item.text ?? '',
719
+ content: stripAnsi(item.text ?? ''),
511
720
  isError: item.status === 'failed' || (item.exitCode !== undefined && item.exitCode !== 0),
512
721
  raw: { method, params },
513
722
  }];
@@ -607,20 +816,75 @@ export function normalizeMcpToolName(server, tool) {
607
816
  // Resolve approval policy from AgentSpawnConfig
608
817
  // ---------------------------------------------------------------------------
609
818
  function resolveApprovalPolicy(config) {
610
- // SUP-1747: Use 'onRequest' for autonomous agents so all tool executions
819
+ // SUP-1747: Use 'on-request' for autonomous agents so all tool executions
611
820
  // flow through the approval bridge for safety evaluation. The bridge
612
821
  // auto-approves safe commands and declines destructive patterns.
822
+ // Codex v0.117+ uses kebab-case: 'on-request' | 'untrusted' | 'on-failure' | 'never'
613
823
  if (config.autonomous)
614
- return 'onRequest';
615
- return 'unlessTrusted';
824
+ return 'on-request';
825
+ return 'untrusted';
616
826
  }
617
- function resolveSandboxPolicy(config) {
827
+ /**
828
+ * Map AgentSpawnConfig sandbox settings to Codex App Server sandbox policy.
829
+ *
830
+ * Codex sandbox levels vs Claude sandbox:
831
+ * | Feature | Claude | Codex |
832
+ * |-----------------------|-------------------------|--------------------------------|
833
+ * | File write control | Per-file glob patterns | Workspace root only |
834
+ * | Network access | Per-domain allow-lists | All-or-nothing per level |
835
+ * | Tool-level permissions| Per-tool allow/deny | Not supported (approval policy)|
836
+ * | Custom writable paths | Multiple glob patterns | Single writableRoots array |
837
+ * | Process isolation | macOS sandbox-exec | Docker/firewall container |
838
+ *
839
+ * Key limitation: Codex cannot restrict writes to specific subdirectories within
840
+ * the workspace or allow network access to specific domains. The mapping is intent-based:
841
+ * "safe browsing/analysis" → readOnly
842
+ * "normal development" → workspaceWrite
843
+ * "install/deploy/admin" → dangerFullAccess
844
+ */
845
+ /**
846
+ * Resolve sandbox policy as an object for turn/start (supports writableRoots).
847
+ * Codex v0.117+ turn/start accepts: { type: 'workspaceWrite', writableRoots: [...] }
848
+ *
849
+ * Network access is enabled by default for agents because they need to run
850
+ * commands like `gh`, `curl`, `pnpm install`, etc. The sandbox still restricts
851
+ * file writes to the workspace root.
852
+ */
853
+ export function resolveSandboxPolicy(config) {
854
+ if (config.sandboxLevel) {
855
+ switch (config.sandboxLevel) {
856
+ case 'read-only':
857
+ return { type: 'readOnly', networkAccess: true };
858
+ case 'workspace-write':
859
+ return { type: 'workspaceWrite', writableRoots: [config.cwd], networkAccess: true };
860
+ case 'full-access':
861
+ return { type: 'dangerFullAccess' };
862
+ }
863
+ }
864
+ // Fallback: boolean sandboxEnabled → workspaceWrite with network
618
865
  if (!config.sandboxEnabled)
619
866
  return undefined;
620
- return {
621
- type: 'workspaceWrite',
622
- writableRoots: [config.cwd],
623
- };
867
+ return { type: 'workspaceWrite', writableRoots: [config.cwd], networkAccess: true };
868
+ }
869
+ /**
870
+ * Resolve sandbox mode as a simple string for thread/start.
871
+ * Codex v0.117+ thread/start accepts: 'read-only' | 'workspace-write' | 'danger-full-access'
872
+ */
873
+ export function resolveSandboxMode(config) {
874
+ if (config.sandboxLevel) {
875
+ switch (config.sandboxLevel) {
876
+ case 'read-only':
877
+ return 'read-only';
878
+ case 'workspace-write':
879
+ return 'workspace-write';
880
+ case 'full-access':
881
+ return 'danger-full-access';
882
+ }
883
+ }
884
+ // Fallback: boolean sandboxEnabled → workspace-write
885
+ if (!config.sandboxEnabled)
886
+ return undefined;
887
+ return 'workspace-write';
624
888
  }
625
889
  // ---------------------------------------------------------------------------
626
890
  // Base Instructions Builder (SUP-1746)
@@ -663,8 +927,10 @@ class AppServerAgentHandle {
663
927
  resumeThreadId;
664
928
  mapperState = {
665
929
  sessionId: null,
930
+ model: null,
666
931
  totalInputTokens: 0,
667
932
  totalOutputTokens: 0,
933
+ totalCachedInputTokens: 0,
668
934
  turnCount: 0,
669
935
  };
670
936
  activeTurnId = null;
@@ -673,6 +939,8 @@ class AppServerAgentHandle {
673
939
  streamEnded = false;
674
940
  /** True while we're waiting for a possible injected turn between turns */
675
941
  awaitingInjection = false;
942
+ /** Accumulated assistant text for the result message (completion comment) */
943
+ accumulatedText = '';
676
944
  constructor(processManager, config, resumeThreadId) {
677
945
  this.processManager = processManager;
678
946
  this.config = config;
@@ -725,7 +993,7 @@ class AppServerAgentHandle {
725
993
  }
726
994
  await this.processManager.request('turn/steer', {
727
995
  threadId: this.sessionId,
728
- turnId: this.activeTurnId,
996
+ expectedTurnId: this.activeTurnId,
729
997
  input: [{ type: 'text', text }],
730
998
  });
731
999
  }
@@ -738,9 +1006,10 @@ class AppServerAgentHandle {
738
1006
  *
739
1007
  * Returns a system event if the request was declined, for observability.
740
1008
  */
741
- async handleApprovalRequest(notification) {
1009
+ handleApprovalRequest(notification) {
742
1010
  const params = notification.params ?? {};
743
- const requestId = params.requestId;
1011
+ // Server requests pass _serverRequestId; fall back to requestId for backwards compat
1012
+ const serverRequestId = params._serverRequestId;
744
1013
  const command = params.command;
745
1014
  const filePath = params.filePath;
746
1015
  let decision;
@@ -756,13 +1025,14 @@ class AppServerAgentHandle {
756
1025
  // Unknown approval request — accept by default
757
1026
  decision = { action: 'acceptForSession' };
758
1027
  }
759
- // Respond to the App Server with the approval decision
760
- await this.processManager.request('approval/respond', {
761
- threadId: this.sessionId,
762
- requestId,
763
- decision: decision.action,
764
- reason: decision.reason,
765
- });
1028
+ // Respond to the server request with the approval decision.
1029
+ // Codex sends approval requests as JSON-RPC server requests (with `id`),
1030
+ // expecting a JSON-RPC response matching that id.
1031
+ if (serverRequestId != null) {
1032
+ this.processManager.respondToServerRequest(serverRequestId, {
1033
+ decision: decision.action,
1034
+ });
1035
+ }
766
1036
  // Emit system event for declined approvals (observability)
767
1037
  if (decision.action === 'decline') {
768
1038
  const target = command ?? filePath ?? 'unknown';
@@ -789,9 +1059,7 @@ class AppServerAgentHandle {
789
1059
  cwd: this.config.cwd,
790
1060
  approvalPolicy: resolveApprovalPolicy(this.config),
791
1061
  };
792
- if (this.config.maxTurns) {
793
- turnParams.maxTurns = this.config.maxTurns;
794
- }
1062
+ turnParams.model = resolveCodexModel(this.config);
795
1063
  const sandboxPolicy = resolveSandboxPolicy(this.config);
796
1064
  if (sandboxPolicy) {
797
1065
  turnParams.sandboxPolicy = sandboxPolicy;
@@ -818,7 +1086,7 @@ class AppServerAgentHandle {
818
1086
  // Resume existing thread
819
1087
  const result = await this.processManager.request('thread/resume', {
820
1088
  threadId: this.resumeThreadId,
821
- personality: 'concise',
1089
+ personality: 'pragmatic',
822
1090
  });
823
1091
  threadId = result?.thread?.id ?? this.resumeThreadId;
824
1092
  }
@@ -829,15 +1097,17 @@ class AppServerAgentHandle {
829
1097
  approvalPolicy: resolveApprovalPolicy(this.config),
830
1098
  serviceName: 'agentfactory',
831
1099
  };
832
- // SUP-1746: Pass persistent system instructions via `instructions` on thread/start.
1100
+ // SUP-1746: Pass persistent system instructions via `baseInstructions` on thread/start.
833
1101
  // Separates safety rules and project context from per-turn task input.
834
1102
  const instructions = buildBaseInstructions(this.config);
835
1103
  if (instructions) {
836
- threadParams.instructions = instructions;
1104
+ threadParams.baseInstructions = instructions;
837
1105
  }
838
- const sandboxPolicy = resolveSandboxPolicy(this.config);
839
- if (sandboxPolicy) {
840
- threadParams.sandboxPolicy = sandboxPolicy;
1106
+ threadParams.model = resolveCodexModel(this.config);
1107
+ // thread/start uses simple string sandbox mode (not object like turn/start)
1108
+ const sandboxMode = resolveSandboxMode(this.config);
1109
+ if (sandboxMode) {
1110
+ threadParams.sandbox = sandboxMode;
841
1111
  }
842
1112
  const result = await this.processManager.request('thread/start', threadParams);
843
1113
  threadId = result?.thread?.id ?? '';
@@ -852,6 +1122,7 @@ class AppServerAgentHandle {
852
1122
  }
853
1123
  this.sessionId = threadId;
854
1124
  this.mapperState.sessionId = threadId;
1125
+ this.mapperState.model = resolveCodexModel(this.config);
855
1126
  // Subscribe to thread notifications
856
1127
  this.processManager.subscribeThread(threadId, (notification) => {
857
1128
  this.notificationQueue.push(notification);
@@ -873,9 +1144,7 @@ class AppServerAgentHandle {
873
1144
  cwd: this.config.cwd,
874
1145
  approvalPolicy: resolveApprovalPolicy(this.config),
875
1146
  };
876
- if (this.config.maxTurns) {
877
- turnParams.maxTurns = this.config.maxTurns;
878
- }
1147
+ turnParams.model = resolveCodexModel(this.config);
879
1148
  const sandboxPolicy = resolveSandboxPolicy(this.config);
880
1149
  if (sandboxPolicy) {
881
1150
  turnParams.sandboxPolicy = sandboxPolicy;
@@ -905,9 +1174,11 @@ class AppServerAgentHandle {
905
1174
  while (this.notificationQueue.length > 0) {
906
1175
  const notification = this.notificationQueue.shift();
907
1176
  // SUP-1747: Intercept approval requests before other processing.
908
- // The App Server emits these when approvalPolicy is 'onRequest'.
909
- if (notification.method.endsWith('/requestApproval')) {
910
- const deniedEvent = await this.handleApprovalRequest(notification);
1177
+ // Codex sends approvals as server requests with methods like:
1178
+ // item/commandExecution/requestApproval, item/fileChange/requestApproval,
1179
+ // item/permissions/requestApproval, applyPatchApproval, execCommandApproval
1180
+ if (notification.method.includes('pproval') || notification.method.includes('requestApproval')) {
1181
+ const deniedEvent = this.handleApprovalRequest(notification);
911
1182
  if (deniedEvent) {
912
1183
  yield deniedEvent;
913
1184
  }
@@ -928,18 +1199,31 @@ class AppServerAgentHandle {
928
1199
  }
929
1200
  const events = mapAppServerNotification(notification, this.mapperState);
930
1201
  for (const event of events) {
931
- // Intercept turn/completed result events — convert to system events
932
- // so the orchestrator doesn't think the agent is done. Track the last
933
- // turn's outcome so we can emit a proper result when the stream ends.
1202
+ // Intercept turn/completed result events.
1203
+ // In autonomous mode (fleet), emit the result directly to end the session.
1204
+ // In interactive mode, convert to system event to keep the stream alive
1205
+ // for potential message injection.
1206
+ // Accumulate assistant text for the result message / completion comment
1207
+ if (event.type === 'assistant_text' && event.text) {
1208
+ this.accumulatedText += event.text;
1209
+ }
934
1210
  if (event.type === 'result') {
935
- lastTurnSuccess = event.success;
936
- lastTurnErrors = event.errors;
937
- yield {
938
- type: 'system',
939
- subtype: 'turn_result',
940
- message: `Turn ${event.success ? 'succeeded' : 'failed'}${event.errors?.length ? ': ' + event.errors[0] : ''}`,
941
- raw: event.raw,
942
- };
1211
+ if (this.config.autonomous) {
1212
+ // Autonomous: emit result with accumulated text and end stream
1213
+ yield { ...event, message: this.accumulatedText.trim() || undefined };
1214
+ this.streamEnded = true;
1215
+ }
1216
+ else {
1217
+ // Interactive: keep stream alive for injection
1218
+ lastTurnSuccess = event.success;
1219
+ lastTurnErrors = event.errors;
1220
+ yield {
1221
+ type: 'system',
1222
+ subtype: 'turn_result',
1223
+ message: `Turn ${event.success ? 'succeeded' : 'failed'}${event.errors?.length ? ': ' + event.errors[0] : ''}`,
1224
+ raw: event.raw,
1225
+ };
1226
+ }
943
1227
  }
944
1228
  else {
945
1229
  yield event;
@@ -955,14 +1239,17 @@ class AppServerAgentHandle {
955
1239
  catch {
956
1240
  // Best effort
957
1241
  }
958
- // Emit the final result event when the stream ends
1242
+ // Emit the final result event when the stream ends (interactive mode / stop)
959
1243
  yield {
960
1244
  type: 'result',
961
1245
  success: lastTurnSuccess,
1246
+ message: this.accumulatedText.trim() || undefined,
962
1247
  errors: lastTurnErrors,
963
1248
  cost: {
964
1249
  inputTokens: this.mapperState.totalInputTokens || undefined,
965
1250
  outputTokens: this.mapperState.totalOutputTokens || undefined,
1251
+ cachedInputTokens: this.mapperState.totalCachedInputTokens || undefined,
1252
+ totalCostUsd: calculateCostUsd(this.mapperState.totalInputTokens, this.mapperState.totalCachedInputTokens, this.mapperState.totalOutputTokens, this.mapperState.model ?? undefined),
966
1253
  numTurns: this.mapperState.turnCount || undefined,
967
1254
  },
968
1255
  raw: null,