@parall/codex-agent 1.42.1 → 1.43.0
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/dist/dispatch.d.ts +18 -0
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +42 -0
- package/dist/index.js +41 -4
- package/dist/workspace.d.ts +2 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +27 -6
- package/package.json +4 -4
- package/src/dispatch.ts +54 -0
- package/src/index.ts +41 -2
- package/src/workspace.ts +36 -9
package/dist/dispatch.d.ts
CHANGED
|
@@ -7,6 +7,14 @@ type CodexAppServerAdapterOptions = Pick<CodexAgentConfig, 'approvalPolicy' | 'c
|
|
|
7
7
|
contextFilePath?: string;
|
|
8
8
|
contextDirPath?: string;
|
|
9
9
|
useParallProvider?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Channel-capability shim directory (agent-core's capabilityBinDir),
|
|
12
|
+
* prepended to the subprocess PATH unconditionally. The DIRECTORY is
|
|
13
|
+
* constant for the bridge's lifetime; its CONTENT tracks capability
|
|
14
|
+
* grants, so a grant or revocation reaches even a long-lived app-server
|
|
15
|
+
* on its next shell command — no respawn needed.
|
|
16
|
+
*/
|
|
17
|
+
capabilityBinDir?: string;
|
|
10
18
|
};
|
|
11
19
|
export declare class CodexAppServerAdapter implements DispatchAdapter {
|
|
12
20
|
private readonly opts;
|
|
@@ -41,6 +49,16 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
41
49
|
forkSession({ sessionKey: parentSessionKey }: ForkOpts): Promise<ForkSessionHandle | null>;
|
|
42
50
|
cleanupFork({ fork }: CleanupForkOpts): void;
|
|
43
51
|
private logForkFailure;
|
|
52
|
+
/**
|
|
53
|
+
* Lazily restart the app-server before the NEXT turn: the workspace
|
|
54
|
+
* config.toml (developer_instructions, carrying capability fragments) is
|
|
55
|
+
* loaded at process start, so a changed fragment set needs a respawn to
|
|
56
|
+
* reach the prompt. Deferred to the next dispatch with no active turns —
|
|
57
|
+
* never kills an in-flight turn; thread state survives via thread/resume.
|
|
58
|
+
*/
|
|
59
|
+
requestProcessRestart(): void;
|
|
60
|
+
private restartRequested;
|
|
61
|
+
private applyPendingRestart;
|
|
44
62
|
stop(): Promise<void>;
|
|
45
63
|
private ensureStarted;
|
|
46
64
|
private doStart;
|
package/dist/dispatch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,KAAK,4BAA4B,GAAG,IAAI,CACtC,gBAAgB,EACd,gBAAgB,GAChB,UAAU,GACV,WAAW,GACX,OAAO,GACP,iBAAiB,GACjB,SAAS,GACT,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,mBAAmB,CAAC;IACpC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAqCF,qBAAa,qBAAsB,YAAW,eAAe;IA+B/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IA9BjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,IAAI,CAA+C;IAC3D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6B;IAC/D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;gBAYQ,IAAI,EAAE,4BAA4B;IAE/D,YAAY,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAMhF,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqB/E,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAUvC,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI1C,QAAQ,CAAC,EACd,KAAK,EACL,YAAY,EACZ,UAAU,EACV,OAAO,GACR,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IA8P7C,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMjD,WAAW,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAgChG,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,cAAc;IAKtB;;;;;;OAMG;IACH,qBAAqB,IAAI,IAAI;IAI7B,OAAO,CAAC,gBAAgB,CAAS;YAEnB,mBAAmB;IAS3B,IAAI;YAuBI,aAAa;YA2Bb,OAAO;IAuGrB,OAAO,CAAC,qBAAqB;YAmCf,UAAU;IA6CxB,OAAO,CAAC,iBAAiB;CA2B1B"}
|
package/dist/dispatch.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync, spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
4
5
|
import { appendPreparedLocalAttachmentRefs, ensureLocalAttachmentGitExclude, pinLocalAttachmentPaths, } from '@parall/agent-core/internal/attachment-input';
|
|
5
6
|
import { normalizeApprovalPolicy, normalizeSandbox } from './config.js';
|
|
6
7
|
import { EventMapper } from './event-mapping.js';
|
|
@@ -125,6 +126,7 @@ export class CodexAppServerAdapter {
|
|
|
125
126
|
}
|
|
126
127
|
(this.opts.log ?? context.log)?.warn?.(`pending steer invalidated (subprocess died); falling through to normal dispatch`);
|
|
127
128
|
}
|
|
129
|
+
await this.applyPendingRestart(context.log);
|
|
128
130
|
await this.ensureStarted(context.log);
|
|
129
131
|
// After ensureStarted resolves the subprocess could still die before we
|
|
130
132
|
// capture the client (handleSubprocessClose nulls this.client). Yield a
|
|
@@ -405,6 +407,24 @@ export class CodexAppServerAdapter {
|
|
|
405
407
|
this.opts.log?.warn?.(`thread/fork failed: ${errToString(err)}`);
|
|
406
408
|
return null;
|
|
407
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* Lazily restart the app-server before the NEXT turn: the workspace
|
|
412
|
+
* config.toml (developer_instructions, carrying capability fragments) is
|
|
413
|
+
* loaded at process start, so a changed fragment set needs a respawn to
|
|
414
|
+
* reach the prompt. Deferred to the next dispatch with no active turns —
|
|
415
|
+
* never kills an in-flight turn; thread state survives via thread/resume.
|
|
416
|
+
*/
|
|
417
|
+
requestProcessRestart() {
|
|
418
|
+
this.restartRequested = true;
|
|
419
|
+
}
|
|
420
|
+
restartRequested = false;
|
|
421
|
+
async applyPendingRestart(log) {
|
|
422
|
+
if (!this.restartRequested || this.activeTurns.size > 0)
|
|
423
|
+
return;
|
|
424
|
+
this.restartRequested = false;
|
|
425
|
+
(log ?? this.opts.log)?.info?.('restarting codex app-server to pick up updated developer_instructions');
|
|
426
|
+
await this.stop();
|
|
427
|
+
}
|
|
408
428
|
async stop() {
|
|
409
429
|
this.stopping = true;
|
|
410
430
|
const proc = this.proc;
|
|
@@ -414,6 +434,12 @@ export class CodexAppServerAdapter {
|
|
|
414
434
|
this.initialized = false;
|
|
415
435
|
this.activeTurnIds.clear();
|
|
416
436
|
this.pendingInjections.clear();
|
|
437
|
+
// Resume markers are process-local facts. Clear them HERE, synchronously:
|
|
438
|
+
// the old child's close callback sees the replaced refs and skips its
|
|
439
|
+
// cleanup as stale, so a marker surviving stop() would make the next
|
|
440
|
+
// dispatch treat the persisted thread as already resumed in the NEW
|
|
441
|
+
// process and lose conversation continuity when turn/start rejects.
|
|
442
|
+
this.resumedThreadIds.clear();
|
|
417
443
|
if (client)
|
|
418
444
|
client.dispose(new Error('adapter stopped'));
|
|
419
445
|
if (proc && proc.exitCode === null && proc.signalCode === null) {
|
|
@@ -468,6 +494,22 @@ export class CodexAppServerAdapter {
|
|
|
468
494
|
FORCE_COLOR: '0',
|
|
469
495
|
NO_COLOR: '1',
|
|
470
496
|
};
|
|
497
|
+
// Channel-capability shims resolve ahead of any globally-installed CLI of
|
|
498
|
+
// the same name (the shim owns auth; the real binary is found further
|
|
499
|
+
// down the PATH by the shim itself). On Windows env keys are
|
|
500
|
+
// case-insensitive and the host key is usually `Path`; after the
|
|
501
|
+
// plain-object spread a literal `PATH` write would SPLIT the key
|
|
502
|
+
// (undefined which wins in the child), so reuse the parent's casing. On
|
|
503
|
+
// POSIX keys are case-SENSITIVE — match `PATH` exactly.
|
|
504
|
+
if (this.opts.capabilityBinDir) {
|
|
505
|
+
const pathKey = IS_WIN32
|
|
506
|
+
? (Object.keys(env).find((k) => k.toUpperCase() === 'PATH') ?? 'PATH')
|
|
507
|
+
: 'PATH';
|
|
508
|
+
const existing = env[pathKey];
|
|
509
|
+
env[pathKey] = existing
|
|
510
|
+
? `${this.opts.capabilityBinDir}${path.delimiter}${existing}`
|
|
511
|
+
: this.opts.capabilityBinDir;
|
|
512
|
+
}
|
|
471
513
|
if (this.opts.contextFilePath) {
|
|
472
514
|
env.PRLL_CONTEXT_FILE = this.opts.contextFilePath;
|
|
473
515
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as os from 'node:os';
|
|
3
|
-
import { ParallAgentGateway, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, initAgentTelemetry, } from '@parall/agent-core';
|
|
3
|
+
import { ParallAgentGateway, capabilityBinDir, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, materializeChannelCapabilities, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, initAgentTelemetry, } from '@parall/agent-core';
|
|
4
4
|
import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
|
|
5
5
|
import { buildCodexRuntimeKey, contextFilePathForSession, dispatchContextDirPath, resolveCodexAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from './config.js';
|
|
6
6
|
import { CodexAppServerAdapter } from './dispatch.js';
|
|
7
7
|
import { CodexSessionManager } from './session-manager.js';
|
|
8
|
-
import { ensureCodexWorkspace, ensureParallProvider, ensureWorkspaceTrusted, isParallProxyMode, } from './workspace.js';
|
|
8
|
+
import { ensureCodexWorkspace, ensureParallProvider, ensureWorkspaceTrusted, isParallProxyMode, writeCodexSystemPrompt, } from './workspace.js';
|
|
9
9
|
const log = createLogger('codex-agent');
|
|
10
10
|
let activeLog = log;
|
|
11
11
|
async function getAgentMeWithLegacyFallback(client, orgId) {
|
|
@@ -58,11 +58,11 @@ async function main() {
|
|
|
58
58
|
ensureParallProvider(config.codexHome, config.apiUrl, agentLog);
|
|
59
59
|
agentLog.info('parall custom provider configured (Responses API HTTP/SSE mode)');
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
const agentIdentity = {
|
|
62
62
|
userId: agentUserId,
|
|
63
63
|
displayName: me.display_name,
|
|
64
64
|
description: me.agent_profile?.description ?? undefined,
|
|
65
|
-
}
|
|
65
|
+
};
|
|
66
66
|
const runtimeKey = config.runtimeKey || buildCodexRuntimeKey(agentUserId);
|
|
67
67
|
const mainContextFilePath = contextFilePathForSession(config.stateDir, runtimeKey);
|
|
68
68
|
const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
|
|
@@ -79,6 +79,21 @@ async function main() {
|
|
|
79
79
|
log: agentLog,
|
|
80
80
|
});
|
|
81
81
|
const platformDefaults = await configMgr.fetch();
|
|
82
|
+
// Channel capabilities: ONE call materializes the local artifacts (the
|
|
83
|
+
// lark-cli auth shim under capabilityBinDir) and yields the system-prompt
|
|
84
|
+
// fragments, so declaration and shim can never diverge within a refresh.
|
|
85
|
+
// No freshness gate — a stale (LKG-cached) declaration fail-closes at the
|
|
86
|
+
// mint endpoint, which re-evaluates the grant on every token request.
|
|
87
|
+
const applyChannelCapabilities = () => {
|
|
88
|
+
const caps = configMgr.capabilities();
|
|
89
|
+
materializeChannelCapabilities(config.stateDir, caps, agentLog);
|
|
90
|
+
return caps.map((c) => c.fragment);
|
|
91
|
+
};
|
|
92
|
+
// Assemble the workspace AFTER the first config fetch so
|
|
93
|
+
// developer_instructions carries the capability declarations from boot.
|
|
94
|
+
const bootCapabilityFragments = applyChannelCapabilities();
|
|
95
|
+
let lastCapabilityFragments = bootCapabilityFragments.join('\n\n');
|
|
96
|
+
ensureCodexWorkspace(config.workspaceDir, agentLog, agentIdentity, bootCapabilityFragments);
|
|
82
97
|
// Model precedence: operator PIN (override) > env > server FLOOR. The server
|
|
83
98
|
// now says which it is via model_is_pin (deriveModelIsPin presence-gates the
|
|
84
99
|
// dual-read: old servers omit it → fall back to model_management). A PIN beats
|
|
@@ -104,6 +119,7 @@ async function main() {
|
|
|
104
119
|
contextFilePath: mainContextFilePath,
|
|
105
120
|
contextDirPath: dispatchContextDirPath(config.stateDir),
|
|
106
121
|
useParallProvider,
|
|
122
|
+
capabilityBinDir: capabilityBinDir(config.stateDir),
|
|
107
123
|
});
|
|
108
124
|
// Shared by onConfigUpdate + onSessionReady. /agents/me is only consumed as
|
|
109
125
|
// deriveModelIsPin's legacy fallback when the server omits model_is_pin
|
|
@@ -129,6 +145,24 @@ async function main() {
|
|
|
129
145
|
model: resolveRuntimeModel(isPin, updated.model, config.model) ?? null,
|
|
130
146
|
reasoningEffort: updated.thinkingEffort ?? config.reasoningEffort ?? null,
|
|
131
147
|
});
|
|
148
|
+
// Capability heat-update: re-materialize shims + rewrite the prompt
|
|
149
|
+
// surfaces. The write is refresh-tolerant (warn + retry next refresh);
|
|
150
|
+
// only a SUCCESSFUL write with a changed fragment set schedules the
|
|
151
|
+
// lazy app-server restart (developer_instructions loads at spawn).
|
|
152
|
+
const fragments = applyChannelCapabilities();
|
|
153
|
+
const joinedFragments = fragments.join('\n\n');
|
|
154
|
+
let promptWritten = true;
|
|
155
|
+
try {
|
|
156
|
+
writeCodexSystemPrompt(config.workspaceDir, agentIdentity, fragments);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
promptWritten = false;
|
|
160
|
+
agentLog.warn(`system prompt refresh write failed (retrying next refresh): ${String(err)}`);
|
|
161
|
+
}
|
|
162
|
+
if (promptWritten && joinedFragments !== lastCapabilityFragments) {
|
|
163
|
+
lastCapabilityFragments = joinedFragments;
|
|
164
|
+
adapter.requestProcessRestart();
|
|
165
|
+
}
|
|
132
166
|
};
|
|
133
167
|
const gateway = new ParallAgentGateway({
|
|
134
168
|
accountId: agentUserId,
|
|
@@ -152,6 +186,9 @@ async function main() {
|
|
|
152
186
|
},
|
|
153
187
|
dispatchAdapter: adapter,
|
|
154
188
|
log: agentLog,
|
|
189
|
+
// Live capability view for hint routing: the channel reply hint points
|
|
190
|
+
// at the vendor CLI only while the grant is active.
|
|
191
|
+
getCapabilityKeys: () => configMgr.capabilities().map((c) => c.key),
|
|
155
192
|
shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
|
|
156
193
|
forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
|
|
157
194
|
dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
|
package/dist/workspace.d.ts
CHANGED
|
@@ -120,7 +120,8 @@ export declare function isParallProxyMode(env?: NodeJS.ProcessEnv): boolean;
|
|
|
120
120
|
export declare function ensureParallProvider(codexHome: string, apiUrl: string, log?: {
|
|
121
121
|
warn: (msg: string) => void;
|
|
122
122
|
}): void;
|
|
123
|
+
export declare function writeCodexSystemPrompt(workspaceDir: string, agentIdentity?: AgentIdentity, capabilityFragments?: string[]): void;
|
|
123
124
|
export declare function ensureCodexWorkspace(workspaceDir: string, log?: {
|
|
124
125
|
warn: (msg: string) => void;
|
|
125
|
-
}, agentIdentity?: AgentIdentity): void;
|
|
126
|
+
}, agentIdentity?: AgentIdentity, capabilityFragments?: string[]): void;
|
|
126
127
|
//# sourceMappingURL=workspace.d.ts.map
|
package/dist/workspace.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;CAI/B,CAAC;AAUF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAAG,SAAS,EAChD,EAAE,EAAE,MAAM,IAAI,GACb,IAAI,CAkJN;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,EAAE;IACnC,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAuJP;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACpC,IAAI,CAEN;AAqGD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAK/E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACpC,IAAI,CAEN;
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;CAI/B,CAAC;AAUF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAAG,SAAS,EAChD,EAAE,EAAE,MAAM,IAAI,GACb,IAAI,CAkJN;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,EAAE;IACnC,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAuJP;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACpC,IAAI,CAEN;AAqGD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAK/E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACpC,IAAI,CAEN;AAoED,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,aAAa,EAC7B,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAC7B,IAAI,CAqBN;AAED,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACrC,aAAa,CAAC,EAAE,aAAa,EAC7B,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAC7B,IAAI,CAQN"}
|
package/dist/workspace.js
CHANGED
|
@@ -585,21 +585,42 @@ function findSectionEnd(content, fromIndex) {
|
|
|
585
585
|
const nextHeader = content.indexOf('\n[', fromIndex);
|
|
586
586
|
return nextHeader === -1 ? content.length : nextHeader;
|
|
587
587
|
}
|
|
588
|
-
|
|
589
|
-
|
|
588
|
+
// writeCodexSystemPrompt (re)writes the standing platform prompt surfaces:
|
|
589
|
+
// .parall/system-prompt.md (human-inspectable copy) and the workspace
|
|
590
|
+
// .codex/config.toml developer_instructions (what the CLI actually loads).
|
|
591
|
+
// capabilityFragments are platform-derived capability declarations
|
|
592
|
+
// (agents.capabilities[].fragment) — placed after the platform reference
|
|
593
|
+
// guide, before skill references; empty/absent = no capability section.
|
|
594
|
+
// Split out from ensureCodexWorkspace so the bridge can rewrite on
|
|
595
|
+
// platform-config heat-update; the app-server reads workspace config at
|
|
596
|
+
// process start, so the caller pairs a CHANGED fragment set with
|
|
597
|
+
// adapter.requestProcessRestart(). Throws on failure BY DESIGN — at boot the
|
|
598
|
+
// prompt is mandatory, so a write failure must fail startup loudly; the
|
|
599
|
+
// refresh hot path wraps this in try/catch and retries next refresh.
|
|
600
|
+
export function writeCodexSystemPrompt(workspaceDir, agentIdentity, capabilityFragments) {
|
|
601
|
+
const parts = [
|
|
590
602
|
buildIdentity(agentIdentity),
|
|
591
603
|
BRIDGE_WORKSPACE_INSTRUCTIONS,
|
|
592
604
|
PRLL_BEHAVIOR,
|
|
593
605
|
PRLL_REFERENCE_GUIDE,
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
606
|
+
];
|
|
607
|
+
if (capabilityFragments && capabilityFragments.length > 0) {
|
|
608
|
+
parts.push(capabilityFragments.join('\n\n'));
|
|
609
|
+
}
|
|
610
|
+
parts.push(buildSkillReferences(workspaceDir));
|
|
611
|
+
const systemPrompt = parts.join('\n\n');
|
|
597
612
|
const parallDir = path.join(workspaceDir, '.parall');
|
|
598
613
|
fs.mkdirSync(parallDir, { recursive: true });
|
|
599
614
|
fs.writeFileSync(path.join(parallDir, 'system-prompt.md'), systemPrompt, 'utf8');
|
|
600
|
-
writeSkillFiles(path.join(parallDir, 'skills'));
|
|
601
615
|
const codexConfigDir = path.join(workspaceDir, '.codex');
|
|
602
616
|
fs.mkdirSync(codexConfigDir, { recursive: true });
|
|
603
617
|
const toml = `developer_instructions = """\n${systemPrompt.replace(/\\/g, '\\\\').replace(/"""/g, '\\"""')}\n"""\n`;
|
|
604
618
|
fs.writeFileSync(path.join(codexConfigDir, 'config.toml'), toml, 'utf8');
|
|
605
619
|
}
|
|
620
|
+
export function ensureCodexWorkspace(workspaceDir, log, agentIdentity, capabilityFragments) {
|
|
621
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
622
|
+
// Boot: let a write failure PROPAGATE (fatal) — startup must not proceed
|
|
623
|
+
// without the platform prompt. The refresh path tolerates failure.
|
|
624
|
+
writeCodexSystemPrompt(workspaceDir, agentIdentity, capabilityFragments);
|
|
625
|
+
writeSkillFiles(path.join(workspaceDir, '.parall', 'skills'));
|
|
626
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/codex-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.43.0",
|
|
4
4
|
"description": "Codex CLI bridge runtime for self-hosted Parall agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"smol-toml": "^1.6.1",
|
|
29
|
-
"@parall/agent-core": "1.
|
|
30
|
-
"@parall/cli": "1.
|
|
31
|
-
"@parall/sdk": "1.
|
|
29
|
+
"@parall/agent-core": "1.43.0",
|
|
30
|
+
"@parall/cli": "1.43.0",
|
|
31
|
+
"@parall/sdk": "1.43.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^22.0.0",
|
package/src/dispatch.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync, spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
4
5
|
import {
|
|
5
6
|
appendPreparedLocalAttachmentRefs,
|
|
6
7
|
ensureLocalAttachmentGitExclude,
|
|
@@ -37,6 +38,14 @@ type CodexAppServerAdapterOptions = Pick<
|
|
|
37
38
|
contextFilePath?: string;
|
|
38
39
|
contextDirPath?: string;
|
|
39
40
|
useParallProvider?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Channel-capability shim directory (agent-core's capabilityBinDir),
|
|
43
|
+
* prepended to the subprocess PATH unconditionally. The DIRECTORY is
|
|
44
|
+
* constant for the bridge's lifetime; its CONTENT tracks capability
|
|
45
|
+
* grants, so a grant or revocation reaches even a long-lived app-server
|
|
46
|
+
* on its next shell command — no respawn needed.
|
|
47
|
+
*/
|
|
48
|
+
capabilityBinDir?: string;
|
|
40
49
|
};
|
|
41
50
|
|
|
42
51
|
type TurnEventEnvelope =
|
|
@@ -174,6 +183,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
174
183
|
);
|
|
175
184
|
}
|
|
176
185
|
|
|
186
|
+
await this.applyPendingRestart(context.log);
|
|
177
187
|
await this.ensureStarted(context.log);
|
|
178
188
|
// After ensureStarted resolves the subprocess could still die before we
|
|
179
189
|
// capture the client (handleSubprocessClose nulls this.client). Yield a
|
|
@@ -453,6 +463,28 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
453
463
|
return null;
|
|
454
464
|
}
|
|
455
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Lazily restart the app-server before the NEXT turn: the workspace
|
|
468
|
+
* config.toml (developer_instructions, carrying capability fragments) is
|
|
469
|
+
* loaded at process start, so a changed fragment set needs a respawn to
|
|
470
|
+
* reach the prompt. Deferred to the next dispatch with no active turns —
|
|
471
|
+
* never kills an in-flight turn; thread state survives via thread/resume.
|
|
472
|
+
*/
|
|
473
|
+
requestProcessRestart(): void {
|
|
474
|
+
this.restartRequested = true;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private restartRequested = false;
|
|
478
|
+
|
|
479
|
+
private async applyPendingRestart(log?: GatewayLogger): Promise<void> {
|
|
480
|
+
if (!this.restartRequested || this.activeTurns.size > 0) return;
|
|
481
|
+
this.restartRequested = false;
|
|
482
|
+
(log ?? this.opts.log)?.info?.(
|
|
483
|
+
'restarting codex app-server to pick up updated developer_instructions',
|
|
484
|
+
);
|
|
485
|
+
await this.stop();
|
|
486
|
+
}
|
|
487
|
+
|
|
456
488
|
async stop() {
|
|
457
489
|
this.stopping = true;
|
|
458
490
|
const proc = this.proc;
|
|
@@ -462,6 +494,12 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
462
494
|
this.initialized = false;
|
|
463
495
|
this.activeTurnIds.clear();
|
|
464
496
|
this.pendingInjections.clear();
|
|
497
|
+
// Resume markers are process-local facts. Clear them HERE, synchronously:
|
|
498
|
+
// the old child's close callback sees the replaced refs and skips its
|
|
499
|
+
// cleanup as stale, so a marker surviving stop() would make the next
|
|
500
|
+
// dispatch treat the persisted thread as already resumed in the NEW
|
|
501
|
+
// process and lose conversation continuity when turn/start rejects.
|
|
502
|
+
this.resumedThreadIds.clear();
|
|
465
503
|
if (client) client.dispose(new Error('adapter stopped'));
|
|
466
504
|
if (proc && proc.exitCode === null && proc.signalCode === null) {
|
|
467
505
|
if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
|
|
@@ -515,6 +553,22 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
515
553
|
FORCE_COLOR: '0',
|
|
516
554
|
NO_COLOR: '1',
|
|
517
555
|
};
|
|
556
|
+
// Channel-capability shims resolve ahead of any globally-installed CLI of
|
|
557
|
+
// the same name (the shim owns auth; the real binary is found further
|
|
558
|
+
// down the PATH by the shim itself). On Windows env keys are
|
|
559
|
+
// case-insensitive and the host key is usually `Path`; after the
|
|
560
|
+
// plain-object spread a literal `PATH` write would SPLIT the key
|
|
561
|
+
// (undefined which wins in the child), so reuse the parent's casing. On
|
|
562
|
+
// POSIX keys are case-SENSITIVE — match `PATH` exactly.
|
|
563
|
+
if (this.opts.capabilityBinDir) {
|
|
564
|
+
const pathKey = IS_WIN32
|
|
565
|
+
? (Object.keys(env).find((k) => k.toUpperCase() === 'PATH') ?? 'PATH')
|
|
566
|
+
: 'PATH';
|
|
567
|
+
const existing = env[pathKey];
|
|
568
|
+
env[pathKey] = existing
|
|
569
|
+
? `${this.opts.capabilityBinDir}${path.delimiter}${existing}`
|
|
570
|
+
: this.opts.capabilityBinDir;
|
|
571
|
+
}
|
|
518
572
|
if (this.opts.contextFilePath) {
|
|
519
573
|
env.PRLL_CONTEXT_FILE = this.opts.contextFilePath;
|
|
520
574
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import {
|
|
5
5
|
ParallAgentGateway,
|
|
6
|
+
capabilityBinDir,
|
|
6
7
|
createPlatformConfigManager,
|
|
7
8
|
createLogger,
|
|
8
9
|
createOtelLogger,
|
|
9
10
|
childLogger,
|
|
10
11
|
deriveModelIsPin,
|
|
12
|
+
materializeChannelCapabilities,
|
|
11
13
|
resolveRuntimeModel,
|
|
12
14
|
parseShutdownDeadlineMs,
|
|
13
15
|
parseForkDeadlineMs,
|
|
@@ -34,6 +36,7 @@ import {
|
|
|
34
36
|
ensureParallProvider,
|
|
35
37
|
ensureWorkspaceTrusted,
|
|
36
38
|
isParallProxyMode,
|
|
39
|
+
writeCodexSystemPrompt,
|
|
37
40
|
} from './workspace.js';
|
|
38
41
|
|
|
39
42
|
const log = createLogger('codex-agent');
|
|
@@ -88,11 +91,11 @@ async function main() {
|
|
|
88
91
|
ensureParallProvider(config.codexHome, config.apiUrl, agentLog);
|
|
89
92
|
agentLog.info('parall custom provider configured (Responses API HTTP/SSE mode)');
|
|
90
93
|
}
|
|
91
|
-
|
|
94
|
+
const agentIdentity = {
|
|
92
95
|
userId: agentUserId,
|
|
93
96
|
displayName: me.display_name,
|
|
94
97
|
description: me.agent_profile?.description ?? undefined,
|
|
95
|
-
}
|
|
98
|
+
};
|
|
96
99
|
const runtimeKey = config.runtimeKey || buildCodexRuntimeKey(agentUserId);
|
|
97
100
|
const mainContextFilePath = contextFilePathForSession(config.stateDir, runtimeKey);
|
|
98
101
|
const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
|
|
@@ -111,6 +114,21 @@ async function main() {
|
|
|
111
114
|
log: agentLog,
|
|
112
115
|
});
|
|
113
116
|
const platformDefaults = await configMgr.fetch();
|
|
117
|
+
// Channel capabilities: ONE call materializes the local artifacts (the
|
|
118
|
+
// lark-cli auth shim under capabilityBinDir) and yields the system-prompt
|
|
119
|
+
// fragments, so declaration and shim can never diverge within a refresh.
|
|
120
|
+
// No freshness gate — a stale (LKG-cached) declaration fail-closes at the
|
|
121
|
+
// mint endpoint, which re-evaluates the grant on every token request.
|
|
122
|
+
const applyChannelCapabilities = () => {
|
|
123
|
+
const caps = configMgr.capabilities();
|
|
124
|
+
materializeChannelCapabilities(config.stateDir, caps, agentLog);
|
|
125
|
+
return caps.map((c) => c.fragment);
|
|
126
|
+
};
|
|
127
|
+
// Assemble the workspace AFTER the first config fetch so
|
|
128
|
+
// developer_instructions carries the capability declarations from boot.
|
|
129
|
+
const bootCapabilityFragments = applyChannelCapabilities();
|
|
130
|
+
let lastCapabilityFragments = bootCapabilityFragments.join('\n\n');
|
|
131
|
+
ensureCodexWorkspace(config.workspaceDir, agentLog, agentIdentity, bootCapabilityFragments);
|
|
114
132
|
// Model precedence: operator PIN (override) > env > server FLOOR. The server
|
|
115
133
|
// now says which it is via model_is_pin (deriveModelIsPin presence-gates the
|
|
116
134
|
// dual-read: old servers omit it → fall back to model_management). A PIN beats
|
|
@@ -143,6 +161,7 @@ async function main() {
|
|
|
143
161
|
contextFilePath: mainContextFilePath,
|
|
144
162
|
contextDirPath: dispatchContextDirPath(config.stateDir),
|
|
145
163
|
useParallProvider,
|
|
164
|
+
capabilityBinDir: capabilityBinDir(config.stateDir),
|
|
146
165
|
});
|
|
147
166
|
|
|
148
167
|
// Shared by onConfigUpdate + onSessionReady. /agents/me is only consumed as
|
|
@@ -172,6 +191,23 @@ async function main() {
|
|
|
172
191
|
model: resolveRuntimeModel(isPin, updated.model, config.model) ?? null,
|
|
173
192
|
reasoningEffort: updated.thinkingEffort ?? config.reasoningEffort ?? null,
|
|
174
193
|
});
|
|
194
|
+
// Capability heat-update: re-materialize shims + rewrite the prompt
|
|
195
|
+
// surfaces. The write is refresh-tolerant (warn + retry next refresh);
|
|
196
|
+
// only a SUCCESSFUL write with a changed fragment set schedules the
|
|
197
|
+
// lazy app-server restart (developer_instructions loads at spawn).
|
|
198
|
+
const fragments = applyChannelCapabilities();
|
|
199
|
+
const joinedFragments = fragments.join('\n\n');
|
|
200
|
+
let promptWritten = true;
|
|
201
|
+
try {
|
|
202
|
+
writeCodexSystemPrompt(config.workspaceDir, agentIdentity, fragments);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
promptWritten = false;
|
|
205
|
+
agentLog.warn(`system prompt refresh write failed (retrying next refresh): ${String(err)}`);
|
|
206
|
+
}
|
|
207
|
+
if (promptWritten && joinedFragments !== lastCapabilityFragments) {
|
|
208
|
+
lastCapabilityFragments = joinedFragments;
|
|
209
|
+
adapter.requestProcessRestart();
|
|
210
|
+
}
|
|
175
211
|
};
|
|
176
212
|
|
|
177
213
|
const gateway = new ParallAgentGateway({
|
|
@@ -196,6 +232,9 @@ async function main() {
|
|
|
196
232
|
},
|
|
197
233
|
dispatchAdapter: adapter,
|
|
198
234
|
log: agentLog,
|
|
235
|
+
// Live capability view for hint routing: the channel reply hint points
|
|
236
|
+
// at the vendor CLI only while the grant is active.
|
|
237
|
+
getCapabilityKeys: () => configMgr.capabilities().map((c) => c.key),
|
|
199
238
|
shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
|
|
200
239
|
forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
|
|
201
240
|
dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
|
package/src/workspace.ts
CHANGED
|
@@ -609,29 +609,56 @@ function findSectionEnd(content: string, fromIndex: number): number {
|
|
|
609
609
|
return nextHeader === -1 ? content.length : nextHeader;
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
-
|
|
612
|
+
// writeCodexSystemPrompt (re)writes the standing platform prompt surfaces:
|
|
613
|
+
// .parall/system-prompt.md (human-inspectable copy) and the workspace
|
|
614
|
+
// .codex/config.toml developer_instructions (what the CLI actually loads).
|
|
615
|
+
// capabilityFragments are platform-derived capability declarations
|
|
616
|
+
// (agents.capabilities[].fragment) — placed after the platform reference
|
|
617
|
+
// guide, before skill references; empty/absent = no capability section.
|
|
618
|
+
// Split out from ensureCodexWorkspace so the bridge can rewrite on
|
|
619
|
+
// platform-config heat-update; the app-server reads workspace config at
|
|
620
|
+
// process start, so the caller pairs a CHANGED fragment set with
|
|
621
|
+
// adapter.requestProcessRestart(). Throws on failure BY DESIGN — at boot the
|
|
622
|
+
// prompt is mandatory, so a write failure must fail startup loudly; the
|
|
623
|
+
// refresh hot path wraps this in try/catch and retries next refresh.
|
|
624
|
+
export function writeCodexSystemPrompt(
|
|
613
625
|
workspaceDir: string,
|
|
614
|
-
log?: { warn: (msg: string) => void },
|
|
615
626
|
agentIdentity?: AgentIdentity,
|
|
627
|
+
capabilityFragments?: string[],
|
|
616
628
|
): void {
|
|
617
|
-
const
|
|
629
|
+
const parts = [
|
|
618
630
|
buildIdentity(agentIdentity),
|
|
619
631
|
BRIDGE_WORKSPACE_INSTRUCTIONS,
|
|
620
632
|
PRLL_BEHAVIOR,
|
|
621
633
|
PRLL_REFERENCE_GUIDE,
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
634
|
+
];
|
|
635
|
+
if (capabilityFragments && capabilityFragments.length > 0) {
|
|
636
|
+
parts.push(capabilityFragments.join('\n\n'));
|
|
637
|
+
}
|
|
638
|
+
parts.push(buildSkillReferences(workspaceDir));
|
|
639
|
+
const systemPrompt = parts.join('\n\n');
|
|
626
640
|
|
|
627
641
|
const parallDir = path.join(workspaceDir, '.parall');
|
|
628
642
|
fs.mkdirSync(parallDir, { recursive: true });
|
|
629
643
|
fs.writeFileSync(path.join(parallDir, 'system-prompt.md'), systemPrompt, 'utf8');
|
|
630
644
|
|
|
631
|
-
writeSkillFiles(path.join(parallDir, 'skills'));
|
|
632
|
-
|
|
633
645
|
const codexConfigDir = path.join(workspaceDir, '.codex');
|
|
634
646
|
fs.mkdirSync(codexConfigDir, { recursive: true });
|
|
635
647
|
const toml = `developer_instructions = """\n${systemPrompt.replace(/\\/g, '\\\\').replace(/"""/g, '\\"""')}\n"""\n`;
|
|
636
648
|
fs.writeFileSync(path.join(codexConfigDir, 'config.toml'), toml, 'utf8');
|
|
637
649
|
}
|
|
650
|
+
|
|
651
|
+
export function ensureCodexWorkspace(
|
|
652
|
+
workspaceDir: string,
|
|
653
|
+
log?: { warn: (msg: string) => void },
|
|
654
|
+
agentIdentity?: AgentIdentity,
|
|
655
|
+
capabilityFragments?: string[],
|
|
656
|
+
): void {
|
|
657
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
658
|
+
|
|
659
|
+
// Boot: let a write failure PROPAGATE (fatal) — startup must not proceed
|
|
660
|
+
// without the platform prompt. The refresh path tolerates failure.
|
|
661
|
+
writeCodexSystemPrompt(workspaceDir, agentIdentity, capabilityFragments);
|
|
662
|
+
|
|
663
|
+
writeSkillFiles(path.join(workspaceDir, '.parall', 'skills'));
|
|
664
|
+
}
|