@khorsheed/dsh-local-agent-codex 0.2.0-rc.1 → 0.2.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/CHANGELOG.md +25 -0
- package/README.en.md +23 -122
- package/README.i18n.yaml +2 -2
- package/README.md +23 -122
- package/lib/client.js +811 -317
- package/lib/index.js +818 -252
- package/lib/types/client/SettingsCard.d.ts +73 -24
- package/lib/types/client/SettingsCard.js +110 -140
- package/lib/types/client/icons.d.ts +1 -1
- package/lib/types/client/index.d.ts +6 -26
- package/lib/types/client/index.js +136 -50
- package/lib/types/client/locales.d.ts +34 -24
- package/lib/types/client/locales.js +38 -28
- package/lib/types/codex-cli-provider.d.ts +54 -8
- package/lib/types/codex-cli-provider.js +204 -70
- package/lib/types/endpoint-auth.d.ts +194 -0
- package/lib/types/endpoint-auth.js +300 -0
- package/lib/types/index.d.ts +19 -3
- package/lib/types/index.js +127 -28
- package/lib/types/live-driver.d.ts +9 -5
- package/lib/types/live-driver.js +247 -108
- package/lib/types/live-switch.d.ts +7 -0
- package/lib/types/live-switch.js +1 -0
- package/lib/types/model-broker.d.ts +12 -2
- package/lib/types/model-broker.js +51 -6
- package/lib/types/model-catalog.d.ts +53 -9
- package/lib/types/model-catalog.js +95 -12
- package/lib/types/provision.d.ts +12 -27
- package/lib/types/provision.js +19 -40
- package/lib/types/records.d.ts +6 -11
- package/lib/types/records.js +14 -25
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +20 -21
- package/lib/invariant.js +0 -31
- package/lib/types/invariant.d.ts +0 -16
- package/lib/types/invariant.js +0 -33
package/lib/types/live-driver.js
CHANGED
|
@@ -24,10 +24,12 @@
|
|
|
24
24
|
import { randomUUID } from 'node:crypto';
|
|
25
25
|
import { StringDecoder } from 'node:string_decoder';
|
|
26
26
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
27
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
27
28
|
import { settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
|
|
28
29
|
import { delegationEnv, persistChildSession, LiveStreamPublisher, LiveFlush, LIVE_FLUSH_INTERVAL_MS } from '@khorsheed/dsh-local-agent';
|
|
29
|
-
import { appendCodexTranscriptLine, codexAssistantEvent, codexLineText, DEFAULT_DISPOSE_GRACE_MS, registerCodexMemberRun, textTask, } from './codex-cli-provider.js';
|
|
30
|
+
import { appendCodexRequestHeader, appendCodexTranscriptLine, codexAssistantEvent, codexLineText, DEFAULT_DISPOSE_GRACE_MS, registerCodexMemberRun, textTask, } from './codex-cli-provider.js';
|
|
30
31
|
import { codexRolloutRoundFacts } from './records.js';
|
|
32
|
+
import { endpointAuthEnv, endpointConfigOverrides } from './endpoint-auth.js';
|
|
31
33
|
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
32
34
|
export const DEFAULT_LIVE_IDLE_MS = 30 * 60_000;
|
|
33
35
|
/** Default timeout for a wire request acknowledgement. */
|
|
@@ -44,11 +46,6 @@ const RECLAIM_EOF_GRACE_MS = 1_000;
|
|
|
44
46
|
export const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = LIVE_FLUSH_INTERVAL_MS;
|
|
45
47
|
/** Default minimum text growth between one streaming item's snapshot messages. */
|
|
46
48
|
export const DEFAULT_SNAPSHOT_MIN_CHARS = 0;
|
|
47
|
-
/**
|
|
48
|
-
* The app-server channel could not come up (spawn failure or handshake
|
|
49
|
-
* timeout/mismatch). The provider catches exactly this and falls back to the
|
|
50
|
-
* exec one-shot; any other error is a real round failure.
|
|
51
|
-
*/
|
|
52
49
|
export class LiveChannelUnavailableError extends Error {
|
|
53
50
|
constructor(message) {
|
|
54
51
|
super(message);
|
|
@@ -137,16 +134,27 @@ export function codexAppServerItemToLine(item) {
|
|
|
137
134
|
};
|
|
138
135
|
}
|
|
139
136
|
case 'dynamicToolCall': {
|
|
140
|
-
// Non-shell tools (e.g. the multi-agent `wait
|
|
137
|
+
// Non-shell tools (e.g. the multi-agent `wait`, the deferred `exec`
|
|
138
|
+
// discovery tool): {id, tool, arguments, status, contentItems, success}
|
|
139
|
+
// — the app-server carries the result INLINE on the completed item.
|
|
141
140
|
const tool = typeof item['tool'] === 'string' ? item.tool : 'dynamic';
|
|
142
141
|
const args = item['arguments'] === undefined || item['arguments'] === null
|
|
143
142
|
? undefined
|
|
144
143
|
: typeof item['arguments'] === 'string' ? item['arguments'] : JSON.stringify(item['arguments']);
|
|
144
|
+
const content = item['contentItems'];
|
|
145
|
+
const result = Array.isArray(content)
|
|
146
|
+
? content
|
|
147
|
+
.filter((block) => typeof block === 'object' && block !== null)
|
|
148
|
+
.map(block => typeof block['text'] === 'string' ? block['text'] : undefined)
|
|
149
|
+
.filter((text) => text !== undefined && text !== '')
|
|
150
|
+
.join('\n')
|
|
151
|
+
: undefined;
|
|
145
152
|
return {
|
|
146
153
|
kind: 'tool',
|
|
147
154
|
id: itemId ?? `codex-live-${randomUUID()}`,
|
|
148
155
|
name: tool,
|
|
149
156
|
...args === undefined || args === '' ? {} : { args },
|
|
157
|
+
...result === undefined || result === '' ? {} : { result },
|
|
150
158
|
};
|
|
151
159
|
}
|
|
152
160
|
case 'collabAgentToolCall': {
|
|
@@ -174,15 +182,22 @@ export function codexAppServerItemToLine(item) {
|
|
|
174
182
|
function usageFromAppServer(breakdown) {
|
|
175
183
|
const input = Number(breakdown['inputTokens']);
|
|
176
184
|
const cached = Number(breakdown['cachedInputTokens']);
|
|
185
|
+
const cacheWrite = Number(breakdown['cacheWriteInputTokens']);
|
|
177
186
|
const output = Number(breakdown['outputTokens']);
|
|
187
|
+
// Codex's input total already contains both cache buckets (total = input +
|
|
188
|
+
// output); the shared contract keeps the three input buckets disjoint.
|
|
178
189
|
const usage = {
|
|
179
190
|
inputTokens: Number.isFinite(input) && Number.isFinite(cached)
|
|
180
|
-
? Math.max(0, input - cached)
|
|
191
|
+
? Math.max(0, input - cached - (Number.isFinite(cacheWrite) ? cacheWrite : 0))
|
|
181
192
|
: Number.isFinite(input) ? input : 0,
|
|
182
193
|
outputTokens: Number.isFinite(output) ? output : 0,
|
|
183
194
|
};
|
|
184
|
-
|
|
195
|
+
// The field rides every payload (zero included): write it whenever present,
|
|
196
|
+
// so "no hits" never reads as "not reported".
|
|
197
|
+
if (Number.isFinite(cached))
|
|
185
198
|
usage.cacheReadTokens = cached;
|
|
199
|
+
if (Number.isFinite(cacheWrite) && cacheWrite > 0)
|
|
200
|
+
usage.cacheWriteTokens = cacheWrite;
|
|
186
201
|
return usage;
|
|
187
202
|
}
|
|
188
203
|
/**
|
|
@@ -552,10 +567,18 @@ export class CodexLiveDriver {
|
|
|
552
567
|
const startModel = spec.startModel?.trim();
|
|
553
568
|
const model = spec.configuration !== undefined ? spec.configuration.model
|
|
554
569
|
: startModel !== undefined && startModel !== '' ? startModel : this.config.model?.(key)?.trim();
|
|
570
|
+
// The endpoint credential routes the resident process too: with a key the
|
|
571
|
+
// app-server spawns onto the relay's custom provider (the same `-c`
|
|
572
|
+
// overrides the exec path carries), so a live round never consults
|
|
573
|
+
// auth.json either. Read per spawn — a card write reaches the next respawn.
|
|
574
|
+
const endpoint = this.config.endpoint === undefined
|
|
575
|
+
? undefined
|
|
576
|
+
: await this.config.endpoint(spec.homeDir).catch(() => undefined);
|
|
555
577
|
const spawnSpec = {
|
|
556
578
|
argv: [
|
|
557
579
|
'codex', 'app-server',
|
|
558
580
|
...member === undefined ? [] : ['-c', member.configOverride],
|
|
581
|
+
...endpointConfigOverrides(endpoint),
|
|
559
582
|
...model === undefined || model === '' ? [] : ['-c', `model=${JSON.stringify(model)}`],
|
|
560
583
|
...spec.configuration?.effort === undefined ? [] : ['-c', `model_reasoning_effort=${JSON.stringify(spec.configuration.effort)}`],
|
|
561
584
|
'--stdio',
|
|
@@ -563,7 +586,7 @@ export class CodexLiveDriver {
|
|
|
563
586
|
cwd: spec.cwd,
|
|
564
587
|
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' },
|
|
565
588
|
graceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
566
|
-
env: delegationEnv({ CODEX_HOME: spec.homeDir }),
|
|
589
|
+
env: delegationEnv({ CODEX_HOME: spec.homeDir, ...endpointAuthEnv(endpoint) }),
|
|
567
590
|
};
|
|
568
591
|
let child;
|
|
569
592
|
try {
|
|
@@ -772,27 +795,31 @@ export class CodexLiveDriver {
|
|
|
772
795
|
const lines = [];
|
|
773
796
|
let mirrored = 0;
|
|
774
797
|
let usage;
|
|
775
|
-
/**
|
|
776
|
-
let
|
|
798
|
+
/** The cumulative turn total the wire's `tokenUsage.total` names, for the settled report. */
|
|
799
|
+
let totalUsage;
|
|
777
800
|
/**
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
* mid-stream therefore folds ABOVE the stream's step and the projection
|
|
783
|
-
* renders the round's chronological order.
|
|
801
|
+
* Per-call usage by transcript line: each `thread/tokenUsage/updated`
|
|
802
|
+
* notification closes one API call (its items have all completed), and
|
|
803
|
+
* the call's LAST line carries its `tokenUsage.last`. The map is
|
|
804
|
+
* authoritative per line, independent of the flush's withUsage flag.
|
|
784
805
|
*/
|
|
785
|
-
const
|
|
806
|
+
const usageByLine = new Map();
|
|
807
|
+
/** Whether a folded line already carries usage (per-call, or the round's fallback — no backfill event exists). */
|
|
808
|
+
let usageCarried = false;
|
|
786
809
|
/**
|
|
787
|
-
* The streaming items seen this round by item id:
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
810
|
+
* The streaming items seen this round by item id: deltas accumulate into
|
|
811
|
+
* throttled snapshots at the item's step once it may open in the log
|
|
812
|
+
* (previewed live before that) — the host folds repeated settles at one
|
|
813
|
+
* coordinate into one live-updating chat node. The item's completion
|
|
814
|
+
* folds at the same step and finalizes it; an entry whose completion
|
|
815
|
+
* never arrives is force-finalized at settle. A tool that completes
|
|
816
|
+
* mid-stream folds after the stream, keeping the round's chronology.
|
|
794
817
|
*/
|
|
795
818
|
const streams = new Map();
|
|
819
|
+
/** Provisional stream keys (live flush and preview identity), never log steps. */
|
|
820
|
+
let streamKeys = 0;
|
|
821
|
+
/** Line indexes already written (a line may fold ahead of an earlier one it chronologically precedes). */
|
|
822
|
+
const folded = new Set();
|
|
796
823
|
/** The item deltas currently accumulate into (items stream sequentially). */
|
|
797
824
|
let activeStream;
|
|
798
825
|
/** Items/completions that arrived before the turn id was known. */
|
|
@@ -840,6 +867,29 @@ export class CodexLiveDriver {
|
|
|
840
867
|
const streamPublisher = localAgent?.liveStreams === undefined ? undefined
|
|
841
868
|
: new LiveStreamPublisher(localAgent.liveStreams, childSession, turn, persist, error => this.ctx.logger.warn(`live checkpoint failed: ${String(error)}`));
|
|
842
869
|
const liveFlush = new LiveFlush(error => this.ctx.logger.warn(`live mirror flush failed: ${String(error)}`), this.config.snapshotMinIntervalMs ?? DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
|
|
870
|
+
// The step ledger. The log admits steps strictly in sequence — one open
|
|
871
|
+
// at a time, numbered when it is WRITTEN (`lastStep`) — while the round's
|
|
872
|
+
// chronology is kept by start order: a stream sorts before every line
|
|
873
|
+
// that completed after its first delta (`after` = lines completed before
|
|
874
|
+
// it began). A stream's step opens in the log only once it is the oldest
|
|
875
|
+
// unfinished stream and everything before it is written; until then the
|
|
876
|
+
// browser previews it (`preview`), and its fold writes the text anyway.
|
|
877
|
+
let lastStep = 0;
|
|
878
|
+
let openStep;
|
|
879
|
+
const openStreamStep = (stream) => {
|
|
880
|
+
lastStep += 1;
|
|
881
|
+
childSession.append('step/start', { turn, step: lastStep });
|
|
882
|
+
stream.durable = lastStep;
|
|
883
|
+
stream.opened = true;
|
|
884
|
+
openStep = lastStep;
|
|
885
|
+
};
|
|
886
|
+
const closeOpenStep = () => {
|
|
887
|
+
if (openStep === undefined)
|
|
888
|
+
return;
|
|
889
|
+
childSession.append('step/end', { turn, step: openStep });
|
|
890
|
+
openStep = undefined;
|
|
891
|
+
};
|
|
892
|
+
const canOpenStream = (stream) => openStep === undefined && mirrored >= stream.after && streams.keys().next().value === stream.itemId;
|
|
843
893
|
const pendingStreamArrival = new Map();
|
|
844
894
|
const appendStreamSnapshot = (stream, force, interrupted, withUsage = false, final = false) => {
|
|
845
895
|
if (stream.text.trim() === '')
|
|
@@ -854,31 +904,39 @@ export class CodexLiveDriver {
|
|
|
854
904
|
liveFlush.cancel(stream.step);
|
|
855
905
|
const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
|
|
856
906
|
pendingStreamArrival.delete(stream.step);
|
|
857
|
-
if (
|
|
858
|
-
if (
|
|
859
|
-
|
|
860
|
-
|
|
907
|
+
if (!stream.opened) {
|
|
908
|
+
if (canOpenStream(stream))
|
|
909
|
+
openStreamStep(stream);
|
|
910
|
+
else if (!final) {
|
|
911
|
+
// Not its turn in the log yet: show it live, write it later.
|
|
912
|
+
streamPublisher?.preview(stream.step, stream.kind, stream.text, { itemId: stream.itemId, receivedAt });
|
|
913
|
+
return;
|
|
861
914
|
}
|
|
862
|
-
|
|
915
|
+
else {
|
|
916
|
+
// Settle: every line has folded; take the next step.
|
|
917
|
+
closeOpenStep();
|
|
918
|
+
openStreamStep(stream);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (streamPublisher !== undefined && !final) {
|
|
922
|
+
streamPublisher.update(stream.durable, stream.kind, stream.text, { receivedAt });
|
|
863
923
|
stream.lastSnapshotAt = now;
|
|
864
924
|
stream.lastSnapshotLen = stream.text.length;
|
|
865
925
|
return;
|
|
866
926
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
}
|
|
871
|
-
const attachUsage = withUsage && !usageCarried && usage !== undefined;
|
|
927
|
+
// Per-call folds carry the accounting; a final snapshot only falls back
|
|
928
|
+
// to the last-call read when the wire named no per-call usage at all.
|
|
929
|
+
const attachUsage = withUsage && !usageCarried && usageByLine.size === 0 && usage !== undefined;
|
|
872
930
|
childSession.append('assistant/message', {
|
|
873
931
|
turn,
|
|
874
|
-
step: stream.
|
|
932
|
+
step: stream.durable,
|
|
875
933
|
message: codexAssistantEvent([
|
|
876
934
|
stream.kind === 'think'
|
|
877
935
|
? { type: 'reasoning', text: stream.text }
|
|
878
936
|
: { type: 'text', text: stream.text },
|
|
879
937
|
]),
|
|
880
938
|
stream: [],
|
|
881
|
-
...withUsage && !usageCarried && usage !== undefined ? { usage } : {},
|
|
939
|
+
...withUsage && !usageCarried && usageByLine.size === 0 && usage !== undefined ? { usage } : {},
|
|
882
940
|
...interrupted ? { interrupted: true } : {},
|
|
883
941
|
}, { surfaceOp: 'append' });
|
|
884
942
|
if (attachUsage)
|
|
@@ -887,11 +945,21 @@ export class CodexLiveDriver {
|
|
|
887
945
|
stream.lastSnapshotLen = stream.text.length;
|
|
888
946
|
persist();
|
|
889
947
|
};
|
|
948
|
+
/** Close a stream's live presentation once its final text is in the log. */
|
|
949
|
+
const finishStream = (stream) => {
|
|
950
|
+
liveFlush.cancel(stream.step);
|
|
951
|
+
streams.delete(stream.itemId);
|
|
952
|
+
if (activeStream === stream.itemId)
|
|
953
|
+
activeStream = undefined;
|
|
954
|
+
streamPublisher?.finish(stream.step);
|
|
955
|
+
if (stream.durable !== undefined)
|
|
956
|
+
streamPublisher?.finish(stream.durable);
|
|
957
|
+
};
|
|
890
958
|
/**
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
* different item's deltas force one
|
|
894
|
-
*
|
|
959
|
+
* Track one new streaming item: its provisional key (live flush and
|
|
960
|
+
* preview identity) and how many lines had completed when it began. A
|
|
961
|
+
* different item's deltas force one freshness snapshot of the current
|
|
962
|
+
* stream.
|
|
895
963
|
*/
|
|
896
964
|
const reserveStream = (itemId, kind) => {
|
|
897
965
|
if (activeStream !== undefined && activeStream !== itemId) {
|
|
@@ -905,75 +973,96 @@ export class CodexLiveDriver {
|
|
|
905
973
|
}
|
|
906
974
|
let stream = streams.get(itemId);
|
|
907
975
|
if (stream === undefined) {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
stream = { itemId, kind, step, text: '', lastSnapshotAt: 0, lastSnapshotLen: 0, opened: false };
|
|
976
|
+
streamKeys += 1;
|
|
977
|
+
stream = { itemId, kind, step: streamKeys, after: lines.length, text: '', lastSnapshotAt: 0, lastSnapshotLen: 0, opened: false };
|
|
911
978
|
streams.set(itemId, stream);
|
|
912
979
|
}
|
|
913
980
|
activeStream = itemId;
|
|
914
981
|
};
|
|
915
|
-
/** The
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
982
|
+
/** The stream a completed line finalizes, if it was streamed. */
|
|
983
|
+
const streamOfLine = (line) => line === undefined || line.kind === 'tool' ? undefined : streams.get(line.itemId ?? '');
|
|
984
|
+
/** Write lines[index] at the next step(s). */
|
|
985
|
+
const foldLine = (index, lineUsage) => {
|
|
986
|
+
const line = lines[index];
|
|
987
|
+
const stream = streamOfLine(line);
|
|
988
|
+
if (stream !== undefined) {
|
|
989
|
+
// A streamed item finalizes its own step (opening it now if no
|
|
990
|
+
// snapshot ever could) and closes it.
|
|
991
|
+
if (!stream.opened) {
|
|
992
|
+
closeOpenStep();
|
|
993
|
+
openStreamStep(stream);
|
|
994
|
+
}
|
|
995
|
+
const blocks = stream.kind === 'think'
|
|
996
|
+
? [{ type: 'reasoning', text: line.kind === 'tool' ? '' : line.text }]
|
|
997
|
+
: [{ type: 'text', text: line.kind === 'tool' ? '' : line.text }];
|
|
998
|
+
childSession.append('assistant/message', {
|
|
999
|
+
turn,
|
|
1000
|
+
step: stream.durable,
|
|
1001
|
+
message: codexAssistantEvent(blocks),
|
|
1002
|
+
stream: [],
|
|
1003
|
+
...lineUsage === undefined ? {} : { usage: lineUsage },
|
|
1004
|
+
}, { surfaceOp: 'append' });
|
|
1005
|
+
finishStream(stream);
|
|
1006
|
+
closeOpenStep();
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
closeOpenStep();
|
|
1010
|
+
lastStep += 1;
|
|
1011
|
+
appendCodexTranscriptLine(childSession, turn, lastStep, line, lineUsage);
|
|
923
1012
|
}
|
|
924
|
-
|
|
1013
|
+
folded.add(index);
|
|
1014
|
+
localAgent?.reportRunProgress(childSession.id, { kind: 'delta', text: codexLineText(line) });
|
|
925
1015
|
};
|
|
926
|
-
/**
|
|
1016
|
+
/**
|
|
1017
|
+
* Mirror the completed lines before `upto` in chronological order; the
|
|
1018
|
+
* last line is held back until completion. A line waits behind every
|
|
1019
|
+
* stream that began before it completed: that stream's line folds first,
|
|
1020
|
+
* or — while it is still streaming — the mirror stops until it lands.
|
|
1021
|
+
*/
|
|
927
1022
|
const mirrorUpTo = (upto, withUsage) => {
|
|
928
|
-
//
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
//
|
|
1023
|
+
// Per-call usage rides the line its notification stamped, whenever that
|
|
1024
|
+
// line folds. The fallback (a wire that never named per-call usage)
|
|
1025
|
+
// rides the last NON-tool line once — tool/call and tool/result events
|
|
1026
|
+
// carry no usage slot, and a kill mid-command ends the transcript with
|
|
1027
|
+
// a tool line. A carrier mirrored in an earlier flush (before the usage
|
|
1028
|
+
// was knowable) loses the accounting — host 0.1.5 has no usage-backfill
|
|
1029
|
+
// event.
|
|
932
1030
|
let usageIndex = -1;
|
|
933
|
-
if (withUsage) {
|
|
1031
|
+
if (withUsage && usageByLine.size === 0) {
|
|
934
1032
|
for (let index = 0; index < lines.length; index += 1) {
|
|
935
1033
|
if (lines[index]?.kind !== 'tool')
|
|
936
1034
|
usageIndex = index;
|
|
937
1035
|
}
|
|
938
1036
|
}
|
|
939
|
-
|
|
940
|
-
const
|
|
941
|
-
if (
|
|
942
|
-
|
|
1037
|
+
const usageFor = (index) => {
|
|
1038
|
+
const own = usageByLine.get(index);
|
|
1039
|
+
if (own !== undefined) {
|
|
1040
|
+
usageCarried = true;
|
|
1041
|
+
return own;
|
|
1042
|
+
}
|
|
943
1043
|
const lineUsage = withUsage && index === usageIndex ? usage : undefined;
|
|
944
1044
|
if (lineUsage !== undefined)
|
|
945
1045
|
usageCarried = true;
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
childSession.append('assistant/message', {
|
|
962
|
-
turn,
|
|
963
|
-
step: stream.step,
|
|
964
|
-
message: codexAssistantEvent(blocks),
|
|
965
|
-
stream: [],
|
|
966
|
-
...lineUsage === undefined ? {} : { usage: lineUsage },
|
|
967
|
-
}, { surfaceOp: 'append' });
|
|
968
|
-
streamPublisher?.finish(stream.step);
|
|
969
|
-
childSession.append('step/end', { turn, step: stream.step });
|
|
970
|
-
}
|
|
971
|
-
else {
|
|
972
|
-
appendCodexTranscriptLine(childSession, turn, foldStep(index), line, lineUsage);
|
|
1046
|
+
return lineUsage;
|
|
1047
|
+
};
|
|
1048
|
+
outer: for (let index = mirrored; index < upto; index += 1) {
|
|
1049
|
+
if (folded.has(index))
|
|
1050
|
+
continue;
|
|
1051
|
+
const own = streamOfLine(lines[index]);
|
|
1052
|
+
// Streams order among themselves by start; a plain line follows every
|
|
1053
|
+
// stream that began before it completed.
|
|
1054
|
+
const all = [...streams.values()];
|
|
1055
|
+
const before = own === undefined ? all.filter(stream => stream.after <= index) : all.slice(0, all.indexOf(own));
|
|
1056
|
+
for (const earlier of before) {
|
|
1057
|
+
const at = lines.findIndex((line, position) => position > index && position < upto && !folded.has(position) && streamOfLine(line) === earlier);
|
|
1058
|
+
if (at < 0)
|
|
1059
|
+
break outer;
|
|
1060
|
+
foldLine(at, usageFor(at));
|
|
973
1061
|
}
|
|
974
|
-
|
|
975
|
-
localAgent?.reportRunProgress(childSession.id, { kind: 'delta', text: codexLineText(line) });
|
|
1062
|
+
foldLine(index, usageFor(index));
|
|
976
1063
|
}
|
|
1064
|
+
while (folded.has(mirrored))
|
|
1065
|
+
mirrored += 1;
|
|
977
1066
|
if (upto > 0)
|
|
978
1067
|
persist();
|
|
979
1068
|
};
|
|
@@ -983,6 +1072,17 @@ export class CodexLiveDriver {
|
|
|
983
1072
|
// The boundary opens after the accept ack (a definitive reject leaves no
|
|
984
1073
|
// dangling turn/start); the prompt and any early items flush inside it.
|
|
985
1074
|
childSession.append('turn/start', { turn });
|
|
1075
|
+
// The provider's own launch facts are the child log's request config —
|
|
1076
|
+
// codex's notifications never carry model/effort, so the turn/start
|
|
1077
|
+
// request parameters the provider sends are the authoritative source.
|
|
1078
|
+
// The header contract requires a model: without one nothing is recorded.
|
|
1079
|
+
if (spec.configuration?.model !== undefined) {
|
|
1080
|
+
appendCodexRequestHeader(childSession, {
|
|
1081
|
+
provider: 'codex-local',
|
|
1082
|
+
model: spec.configuration.model,
|
|
1083
|
+
...spec.configuration.effort === undefined ? {} : { reasoningEffort: spec.configuration.effort },
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
986
1086
|
childSession.append('user/message', createUserMessage({
|
|
987
1087
|
content: [{ type: 'text', text: task }],
|
|
988
1088
|
source: { kind: 'user' },
|
|
@@ -1010,8 +1110,9 @@ export class CodexLiveDriver {
|
|
|
1010
1110
|
if (line === undefined)
|
|
1011
1111
|
return;
|
|
1012
1112
|
lines.push(line);
|
|
1013
|
-
// Hold back the volatile last line
|
|
1014
|
-
//
|
|
1113
|
+
// Hold back the volatile last line (a tool result may still merge
|
|
1114
|
+
// into it) — per-call usage arrives on thread/tokenUsage/updated and
|
|
1115
|
+
// folds with the line whenever it lands.
|
|
1015
1116
|
mirrorUpTo(lines.length - 1, false);
|
|
1016
1117
|
return;
|
|
1017
1118
|
}
|
|
@@ -1040,19 +1141,41 @@ export class CodexLiveDriver {
|
|
|
1040
1141
|
if (method === 'thread/tokenUsage/updated') {
|
|
1041
1142
|
const tokenUsage = params['tokenUsage'];
|
|
1042
1143
|
const last = tokenUsage?.['last'];
|
|
1043
|
-
|
|
1144
|
+
const total = tokenUsage?.['total'];
|
|
1145
|
+
// A total with no numbers is "not reported" (never override the
|
|
1146
|
+
// per-call read with an empty object).
|
|
1147
|
+
if (total !== undefined && (Number.isFinite(Number(total['inputTokens'])) || Number.isFinite(Number(total['outputTokens'])))) {
|
|
1148
|
+
totalUsage = usageFromAppServer(total);
|
|
1149
|
+
}
|
|
1150
|
+
if (last !== undefined) {
|
|
1044
1151
|
usage = usageFromAppServer(last);
|
|
1152
|
+
// One notification closes one API call (the rollout's token_count
|
|
1153
|
+
// position: right after the call's last item). Stamp the call's own
|
|
1154
|
+
// usage on its last line and flush the completed prefix with it;
|
|
1155
|
+
// the volatile last line keeps its hold (a tool result may still
|
|
1156
|
+
// merge into it) and folds with its mapped usage whenever it lands.
|
|
1157
|
+
if (lines.length > 0) {
|
|
1158
|
+
usageByLine.set(lines.length - 1, usage);
|
|
1159
|
+
mirrorUpTo(lines.length - 1, true);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1045
1162
|
return;
|
|
1046
1163
|
}
|
|
1047
1164
|
if (method === 'turn/completed') {
|
|
1048
1165
|
const turnData = params['turn'];
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1166
|
+
// A fold failure must never skip the completion: the round settles
|
|
1167
|
+
// with its mirrored prefix instead of hanging forever.
|
|
1168
|
+
try {
|
|
1169
|
+
mirrorUpTo(lines.length, true);
|
|
1170
|
+
}
|
|
1171
|
+
finally {
|
|
1172
|
+
resolveCompletion({
|
|
1173
|
+
status: String(turnData['status']),
|
|
1174
|
+
...(turnData['error'] === null || turnData['error'] === undefined
|
|
1175
|
+
? {}
|
|
1176
|
+
: { error: turnData['error'] }),
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1056
1179
|
}
|
|
1057
1180
|
};
|
|
1058
1181
|
/** Route one wire notification: round-scoped, turn-id tagged, early ones buffered. */
|
|
@@ -1174,6 +1297,15 @@ export class CodexLiveDriver {
|
|
|
1174
1297
|
// flush it now (the exec settle-mirror's partial-work contract).
|
|
1175
1298
|
if (mirrored < lines.length)
|
|
1176
1299
|
mirrorUpTo(lines.length, true);
|
|
1300
|
+
// A line still waiting behind a stream whose completion never came:
|
|
1301
|
+
// finalize that stream from its last snapshot, then keep folding.
|
|
1302
|
+
while (mirrored < lines.length && streams.size > 0) {
|
|
1303
|
+
const oldest = streams.values().next().value;
|
|
1304
|
+
appendStreamSnapshot(oldest, true, settled.stopReason !== 'completed', false, true);
|
|
1305
|
+
finishStream(oldest);
|
|
1306
|
+
closeOpenStep();
|
|
1307
|
+
mirrorUpTo(lines.length, true);
|
|
1308
|
+
}
|
|
1177
1309
|
// Streams whose item/completed never arrived (abort, or the server
|
|
1178
1310
|
// ended the turn mid-item) still finalize INSIDE the turn window:
|
|
1179
1311
|
// one forced snapshot each — interrupted on a non-completed round,
|
|
@@ -1183,9 +1315,8 @@ export class CodexLiveDriver {
|
|
|
1183
1315
|
const remaining = [...streams.values()];
|
|
1184
1316
|
for (const [position, stream] of remaining.entries()) {
|
|
1185
1317
|
appendStreamSnapshot(stream, true, settled.stopReason !== 'completed', position === remaining.length - 1, true);
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
childSession.append('step/end', { turn, step: stream.step });
|
|
1318
|
+
finishStream(stream);
|
|
1319
|
+
closeOpenStep();
|
|
1189
1320
|
}
|
|
1190
1321
|
streams.clear();
|
|
1191
1322
|
activeStream = undefined;
|
|
@@ -1238,7 +1369,11 @@ export class CodexLiveDriver {
|
|
|
1238
1369
|
windowStart: startedAtMs,
|
|
1239
1370
|
cwd: spec.cwd,
|
|
1240
1371
|
});
|
|
1241
|
-
|
|
1372
|
+
// The settled report wants the round TOTAL: prefer the wire's
|
|
1373
|
+
// cumulative tokenUsage.total (the per-call `last` under-reports a
|
|
1374
|
+
// multi-call round); the rollout read is the non-completed turn's
|
|
1375
|
+
// fallback, as before.
|
|
1376
|
+
const settledUsage = totalUsage ?? usage ?? facts.usage;
|
|
1242
1377
|
const round = {
|
|
1243
1378
|
...facts.model === undefined ? {} : { observedModel: facts.model },
|
|
1244
1379
|
...facts.cliVersion === undefined ? {} : { cliVersion: facts.cliVersion },
|
|
@@ -1261,7 +1396,11 @@ export class CodexLiveDriver {
|
|
|
1261
1396
|
// hostage to the slow windows — a cancel returns the run immediately.
|
|
1262
1397
|
await Promise.race([accepted, abortBranch.catch(() => undefined)]);
|
|
1263
1398
|
return subprocessRunHandle({
|
|
1264
|
-
|
|
1399
|
+
// Host 0.2.1: a fresh PER-EXECUTION run id — the activation manager
|
|
1400
|
+
// rejects a run id naming a live Session (this child session is live),
|
|
1401
|
+
// and resume rounds reuse the child session id. The family's durable
|
|
1402
|
+
// tracking key (the child session id) rides the staged intent instead.
|
|
1403
|
+
id: SessionId(randomUUID()),
|
|
1265
1404
|
result,
|
|
1266
1405
|
signal: request.signal,
|
|
1267
1406
|
onAbort,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { Context } from '@deepseek-ai/cordis';
|
|
14
14
|
import type { SettingsFace } from '@khorsheed/dsh-local-agent';
|
|
15
15
|
import type { Config } from './index.ts';
|
|
16
|
+
import type { CodexEndpointAuth } from './endpoint-auth.ts';
|
|
16
17
|
import { CodexLiveDriver } from './live-driver.ts';
|
|
17
18
|
import type { CodexLiveMirrorGranularity } from './live-driver.ts';
|
|
18
19
|
/** The resolved live settings (schema defaults ← YAML base ← user layer). */
|
|
@@ -41,6 +42,12 @@ export declare class LiveDriverSwitch {
|
|
|
41
42
|
* outrank the settings value.
|
|
42
43
|
*/
|
|
43
44
|
model?: (childSessionId: string) => string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Per-spawn endpoint credential resolver, handed to every driver
|
|
47
|
+
* generation: a resolution carrying a key routes the app-server at the
|
|
48
|
+
* relay (see the driver's own contract).
|
|
49
|
+
*/
|
|
50
|
+
endpoint?: (scopeHome: string) => Promise<CodexEndpointAuth>;
|
|
44
51
|
});
|
|
45
52
|
/**
|
|
46
53
|
* Resolve the live driver for one round's member (the provider's call
|
package/lib/types/live-switch.js
CHANGED
|
@@ -69,6 +69,7 @@ export class LiveDriverSwitch {
|
|
|
69
69
|
...this.options.sandbox === undefined ? {} : { sandbox: this.options.sandbox },
|
|
70
70
|
...this.options.liveIdleMs === undefined ? {} : { liveIdleMs: this.options.liveIdleMs },
|
|
71
71
|
...this.options.model === undefined ? {} : { model: this.options.model },
|
|
72
|
+
...this.options.endpoint === undefined ? {} : { endpoint: this.options.endpoint },
|
|
72
73
|
})
|
|
73
74
|
: undefined;
|
|
74
75
|
if (retiring !== undefined) {
|
|
@@ -31,8 +31,18 @@ import type { LocalAgentModelBroker, LocalAgentModelInfo, LocalAgentModelDirecto
|
|
|
31
31
|
export interface CodexModelBrokerDeps {
|
|
32
32
|
/** Host context carrying the family registry (in-flight reads, delegation records). */
|
|
33
33
|
readonly ctx: Context;
|
|
34
|
-
/**
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The settings-layer model for one scoped home: the curated model list's
|
|
36
|
+
* FIRST entry (models.json, edited on the card), then the plugin-config
|
|
37
|
+
* `model` key — blank reads unset.
|
|
38
|
+
*/
|
|
39
|
+
readonly settingsModel: (homeDir: string) => string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The scope's curated model list (models.json) for the choice vocabulary:
|
|
42
|
+
* non-empty it is the COMPLETE vocabulary (nothing outside the endpoint's
|
|
43
|
+
* served catalog sneaks in); empty falls back to the pre-list chain.
|
|
44
|
+
*/
|
|
45
|
+
readonly modelList?: (homeDir: string) => readonly string[];
|
|
36
46
|
/** The card's recent-model memory (the suggestion vocabulary's tail). */
|
|
37
47
|
readonly recentModels: () => readonly string[];
|
|
38
48
|
/**
|