@khorsheed/dsh-local-agent-codex 0.2.0-rc.3 → 0.2.1
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 +23 -1
- package/README.en.md +23 -136
- package/README.i18n.yaml +2 -2
- package/README.md +23 -136
- package/lib/client.js +159 -74
- package/lib/index.js +320 -150
- package/lib/types/client/SettingsCard.js +11 -3
- package/lib/types/client/icons.d.ts +1 -1
- package/lib/types/client/index.js +2 -2
- package/lib/types/codex-cli-provider.d.ts +33 -4
- package/lib/types/codex-cli-provider.js +144 -43
- package/lib/types/index.js +4 -2
- package/lib/types/live-driver.d.ts +0 -5
- package/lib/types/live-driver.js +231 -106
- package/lib/types/model-broker.js +42 -1
- package/lib/types/records.d.ts +6 -4
- package/lib/types/records.js +13 -6
- package/package.json +2 -2
package/lib/types/live-driver.js
CHANGED
|
@@ -27,7 +27,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
|
27
27
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
28
28
|
import { settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
|
|
29
29
|
import { delegationEnv, persistChildSession, LiveStreamPublisher, LiveFlush, LIVE_FLUSH_INTERVAL_MS } from '@khorsheed/dsh-local-agent';
|
|
30
|
-
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';
|
|
31
31
|
import { codexRolloutRoundFacts } from './records.js';
|
|
32
32
|
import { endpointAuthEnv, endpointConfigOverrides } from './endpoint-auth.js';
|
|
33
33
|
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
@@ -46,11 +46,6 @@ const RECLAIM_EOF_GRACE_MS = 1_000;
|
|
|
46
46
|
export const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = LIVE_FLUSH_INTERVAL_MS;
|
|
47
47
|
/** Default minimum text growth between one streaming item's snapshot messages. */
|
|
48
48
|
export const DEFAULT_SNAPSHOT_MIN_CHARS = 0;
|
|
49
|
-
/**
|
|
50
|
-
* The app-server channel could not come up (spawn failure or handshake
|
|
51
|
-
* timeout/mismatch). The provider catches exactly this and falls back to the
|
|
52
|
-
* exec one-shot; any other error is a real round failure.
|
|
53
|
-
*/
|
|
54
49
|
export class LiveChannelUnavailableError extends Error {
|
|
55
50
|
constructor(message) {
|
|
56
51
|
super(message);
|
|
@@ -139,16 +134,27 @@ export function codexAppServerItemToLine(item) {
|
|
|
139
134
|
};
|
|
140
135
|
}
|
|
141
136
|
case 'dynamicToolCall': {
|
|
142
|
-
// 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.
|
|
143
140
|
const tool = typeof item['tool'] === 'string' ? item.tool : 'dynamic';
|
|
144
141
|
const args = item['arguments'] === undefined || item['arguments'] === null
|
|
145
142
|
? undefined
|
|
146
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;
|
|
147
152
|
return {
|
|
148
153
|
kind: 'tool',
|
|
149
154
|
id: itemId ?? `codex-live-${randomUUID()}`,
|
|
150
155
|
name: tool,
|
|
151
156
|
...args === undefined || args === '' ? {} : { args },
|
|
157
|
+
...result === undefined || result === '' ? {} : { result },
|
|
152
158
|
};
|
|
153
159
|
}
|
|
154
160
|
case 'collabAgentToolCall': {
|
|
@@ -176,15 +182,22 @@ export function codexAppServerItemToLine(item) {
|
|
|
176
182
|
function usageFromAppServer(breakdown) {
|
|
177
183
|
const input = Number(breakdown['inputTokens']);
|
|
178
184
|
const cached = Number(breakdown['cachedInputTokens']);
|
|
185
|
+
const cacheWrite = Number(breakdown['cacheWriteInputTokens']);
|
|
179
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.
|
|
180
189
|
const usage = {
|
|
181
190
|
inputTokens: Number.isFinite(input) && Number.isFinite(cached)
|
|
182
|
-
? Math.max(0, input - cached)
|
|
191
|
+
? Math.max(0, input - cached - (Number.isFinite(cacheWrite) ? cacheWrite : 0))
|
|
183
192
|
: Number.isFinite(input) ? input : 0,
|
|
184
193
|
outputTokens: Number.isFinite(output) ? output : 0,
|
|
185
194
|
};
|
|
186
|
-
|
|
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))
|
|
187
198
|
usage.cacheReadTokens = cached;
|
|
199
|
+
if (Number.isFinite(cacheWrite) && cacheWrite > 0)
|
|
200
|
+
usage.cacheWriteTokens = cacheWrite;
|
|
188
201
|
return usage;
|
|
189
202
|
}
|
|
190
203
|
/**
|
|
@@ -782,27 +795,31 @@ export class CodexLiveDriver {
|
|
|
782
795
|
const lines = [];
|
|
783
796
|
let mirrored = 0;
|
|
784
797
|
let usage;
|
|
785
|
-
/**
|
|
786
|
-
let
|
|
798
|
+
/** The cumulative turn total the wire's `tokenUsage.total` names, for the settled report. */
|
|
799
|
+
let totalUsage;
|
|
787
800
|
/**
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
* mid-stream therefore folds ABOVE the stream's step and the projection
|
|
793
|
-
* 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.
|
|
794
805
|
*/
|
|
795
|
-
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;
|
|
796
809
|
/**
|
|
797
|
-
* The streaming items seen this round by item id:
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
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.
|
|
804
817
|
*/
|
|
805
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();
|
|
806
823
|
/** The item deltas currently accumulate into (items stream sequentially). */
|
|
807
824
|
let activeStream;
|
|
808
825
|
/** Items/completions that arrived before the turn id was known. */
|
|
@@ -850,6 +867,29 @@ export class CodexLiveDriver {
|
|
|
850
867
|
const streamPublisher = localAgent?.liveStreams === undefined ? undefined
|
|
851
868
|
: new LiveStreamPublisher(localAgent.liveStreams, childSession, turn, persist, error => this.ctx.logger.warn(`live checkpoint failed: ${String(error)}`));
|
|
852
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;
|
|
853
893
|
const pendingStreamArrival = new Map();
|
|
854
894
|
const appendStreamSnapshot = (stream, force, interrupted, withUsage = false, final = false) => {
|
|
855
895
|
if (stream.text.trim() === '')
|
|
@@ -864,31 +904,39 @@ export class CodexLiveDriver {
|
|
|
864
904
|
liveFlush.cancel(stream.step);
|
|
865
905
|
const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
|
|
866
906
|
pendingStreamArrival.delete(stream.step);
|
|
867
|
-
if (
|
|
868
|
-
if (
|
|
869
|
-
|
|
870
|
-
|
|
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;
|
|
871
914
|
}
|
|
872
|
-
|
|
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 });
|
|
873
923
|
stream.lastSnapshotAt = now;
|
|
874
924
|
stream.lastSnapshotLen = stream.text.length;
|
|
875
925
|
return;
|
|
876
926
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
}
|
|
881
|
-
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;
|
|
882
930
|
childSession.append('assistant/message', {
|
|
883
931
|
turn,
|
|
884
|
-
step: stream.
|
|
932
|
+
step: stream.durable,
|
|
885
933
|
message: codexAssistantEvent([
|
|
886
934
|
stream.kind === 'think'
|
|
887
935
|
? { type: 'reasoning', text: stream.text }
|
|
888
936
|
: { type: 'text', text: stream.text },
|
|
889
937
|
]),
|
|
890
938
|
stream: [],
|
|
891
|
-
...withUsage && !usageCarried && usage !== undefined ? { usage } : {},
|
|
939
|
+
...withUsage && !usageCarried && usageByLine.size === 0 && usage !== undefined ? { usage } : {},
|
|
892
940
|
...interrupted ? { interrupted: true } : {},
|
|
893
941
|
}, { surfaceOp: 'append' });
|
|
894
942
|
if (attachUsage)
|
|
@@ -897,11 +945,21 @@ export class CodexLiveDriver {
|
|
|
897
945
|
stream.lastSnapshotLen = stream.text.length;
|
|
898
946
|
persist();
|
|
899
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
|
+
};
|
|
900
958
|
/**
|
|
901
|
-
*
|
|
902
|
-
*
|
|
903
|
-
* different item's deltas force one
|
|
904
|
-
*
|
|
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.
|
|
905
963
|
*/
|
|
906
964
|
const reserveStream = (itemId, kind) => {
|
|
907
965
|
if (activeStream !== undefined && activeStream !== itemId) {
|
|
@@ -915,75 +973,96 @@ export class CodexLiveDriver {
|
|
|
915
973
|
}
|
|
916
974
|
let stream = streams.get(itemId);
|
|
917
975
|
if (stream === undefined) {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
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 };
|
|
921
978
|
streams.set(itemId, stream);
|
|
922
979
|
}
|
|
923
980
|
activeStream = itemId;
|
|
924
981
|
};
|
|
925
|
-
/** The
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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);
|
|
933
1012
|
}
|
|
934
|
-
|
|
1013
|
+
folded.add(index);
|
|
1014
|
+
localAgent?.reportRunProgress(childSession.id, { kind: 'delta', text: codexLineText(line) });
|
|
935
1015
|
};
|
|
936
|
-
/**
|
|
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
|
+
*/
|
|
937
1022
|
const mirrorUpTo = (upto, withUsage) => {
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
//
|
|
941
|
-
//
|
|
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.
|
|
942
1030
|
let usageIndex = -1;
|
|
943
|
-
if (withUsage) {
|
|
1031
|
+
if (withUsage && usageByLine.size === 0) {
|
|
944
1032
|
for (let index = 0; index < lines.length; index += 1) {
|
|
945
1033
|
if (lines[index]?.kind !== 'tool')
|
|
946
1034
|
usageIndex = index;
|
|
947
1035
|
}
|
|
948
1036
|
}
|
|
949
|
-
|
|
950
|
-
const
|
|
951
|
-
if (
|
|
952
|
-
|
|
1037
|
+
const usageFor = (index) => {
|
|
1038
|
+
const own = usageByLine.get(index);
|
|
1039
|
+
if (own !== undefined) {
|
|
1040
|
+
usageCarried = true;
|
|
1041
|
+
return own;
|
|
1042
|
+
}
|
|
953
1043
|
const lineUsage = withUsage && index === usageIndex ? usage : undefined;
|
|
954
1044
|
if (lineUsage !== undefined)
|
|
955
1045
|
usageCarried = true;
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
childSession.append('assistant/message', {
|
|
972
|
-
turn,
|
|
973
|
-
step: stream.step,
|
|
974
|
-
message: codexAssistantEvent(blocks),
|
|
975
|
-
stream: [],
|
|
976
|
-
...lineUsage === undefined ? {} : { usage: lineUsage },
|
|
977
|
-
}, { surfaceOp: 'append' });
|
|
978
|
-
streamPublisher?.finish(stream.step);
|
|
979
|
-
childSession.append('step/end', { turn, step: stream.step });
|
|
980
|
-
}
|
|
981
|
-
else {
|
|
982
|
-
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));
|
|
983
1061
|
}
|
|
984
|
-
|
|
985
|
-
localAgent?.reportRunProgress(childSession.id, { kind: 'delta', text: codexLineText(line) });
|
|
1062
|
+
foldLine(index, usageFor(index));
|
|
986
1063
|
}
|
|
1064
|
+
while (folded.has(mirrored))
|
|
1065
|
+
mirrored += 1;
|
|
987
1066
|
if (upto > 0)
|
|
988
1067
|
persist();
|
|
989
1068
|
};
|
|
@@ -993,6 +1072,17 @@ export class CodexLiveDriver {
|
|
|
993
1072
|
// The boundary opens after the accept ack (a definitive reject leaves no
|
|
994
1073
|
// dangling turn/start); the prompt and any early items flush inside it.
|
|
995
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
|
+
}
|
|
996
1086
|
childSession.append('user/message', createUserMessage({
|
|
997
1087
|
content: [{ type: 'text', text: task }],
|
|
998
1088
|
source: { kind: 'user' },
|
|
@@ -1020,8 +1110,9 @@ export class CodexLiveDriver {
|
|
|
1020
1110
|
if (line === undefined)
|
|
1021
1111
|
return;
|
|
1022
1112
|
lines.push(line);
|
|
1023
|
-
// Hold back the volatile last line
|
|
1024
|
-
//
|
|
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.
|
|
1025
1116
|
mirrorUpTo(lines.length - 1, false);
|
|
1026
1117
|
return;
|
|
1027
1118
|
}
|
|
@@ -1050,19 +1141,41 @@ export class CodexLiveDriver {
|
|
|
1050
1141
|
if (method === 'thread/tokenUsage/updated') {
|
|
1051
1142
|
const tokenUsage = params['tokenUsage'];
|
|
1052
1143
|
const last = tokenUsage?.['last'];
|
|
1053
|
-
|
|
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) {
|
|
1054
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
|
+
}
|
|
1055
1162
|
return;
|
|
1056
1163
|
}
|
|
1057
1164
|
if (method === 'turn/completed') {
|
|
1058
1165
|
const turnData = params['turn'];
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
+
}
|
|
1066
1179
|
}
|
|
1067
1180
|
};
|
|
1068
1181
|
/** Route one wire notification: round-scoped, turn-id tagged, early ones buffered. */
|
|
@@ -1184,6 +1297,15 @@ export class CodexLiveDriver {
|
|
|
1184
1297
|
// flush it now (the exec settle-mirror's partial-work contract).
|
|
1185
1298
|
if (mirrored < lines.length)
|
|
1186
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
|
+
}
|
|
1187
1309
|
// Streams whose item/completed never arrived (abort, or the server
|
|
1188
1310
|
// ended the turn mid-item) still finalize INSIDE the turn window:
|
|
1189
1311
|
// one forced snapshot each — interrupted on a non-completed round,
|
|
@@ -1193,9 +1315,8 @@ export class CodexLiveDriver {
|
|
|
1193
1315
|
const remaining = [...streams.values()];
|
|
1194
1316
|
for (const [position, stream] of remaining.entries()) {
|
|
1195
1317
|
appendStreamSnapshot(stream, true, settled.stopReason !== 'completed', position === remaining.length - 1, true);
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
childSession.append('step/end', { turn, step: stream.step });
|
|
1318
|
+
finishStream(stream);
|
|
1319
|
+
closeOpenStep();
|
|
1199
1320
|
}
|
|
1200
1321
|
streams.clear();
|
|
1201
1322
|
activeStream = undefined;
|
|
@@ -1248,7 +1369,11 @@ export class CodexLiveDriver {
|
|
|
1248
1369
|
windowStart: startedAtMs,
|
|
1249
1370
|
cwd: spec.cwd,
|
|
1250
1371
|
});
|
|
1251
|
-
|
|
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;
|
|
1252
1377
|
const round = {
|
|
1253
1378
|
...facts.model === undefined ? {} : { observedModel: facts.model },
|
|
1254
1379
|
...facts.cliVersion === undefined ? {} : { cliVersion: facts.cliVersion },
|
|
@@ -39,6 +39,42 @@ function dedupeChoices(layers) {
|
|
|
39
39
|
}
|
|
40
40
|
return choices;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Scope the served directory to the surface asking for it (the same helper
|
|
44
|
+
* lives in each provider's broker — fold into the family core when the
|
|
45
|
+
* directory faces next evolve):
|
|
46
|
+
*
|
|
47
|
+
* - memberless (the card's add-model dialog, the invite picker): the
|
|
48
|
+
* endpoint's served catalog only — the configuration/history extras the
|
|
49
|
+
* broker's info extension adds for STATUS display must never become add
|
|
50
|
+
* candidates;
|
|
51
|
+
* - a member whose harness has a curated list: the list IS the vocabulary,
|
|
52
|
+
* in the list's own order (the same rule `choices` already follows) — a
|
|
53
|
+
* curated value the endpoint no longer serves stays as a bare row (the
|
|
54
|
+
* card shows it the same way), and the member's current effective model is
|
|
55
|
+
* kept visible as a bare entry when it sits outside the list, so a pre-list
|
|
56
|
+
* binding never renders blank;
|
|
57
|
+
* - a member without a curated list: the legacy extended vocabulary.
|
|
58
|
+
*/
|
|
59
|
+
function scopeServedDirectory(directory, opts) {
|
|
60
|
+
if (opts.member && opts.curated.length === 0)
|
|
61
|
+
return directory;
|
|
62
|
+
const served = directory.entries.filter(entry => entry.source === 'native');
|
|
63
|
+
if (!opts.member)
|
|
64
|
+
return { ...directory, entries: served };
|
|
65
|
+
const byValue = new Map(served.flatMap(entry => entry.resolvedModel === undefined
|
|
66
|
+
? [[entry.value, entry]]
|
|
67
|
+
: [[entry.value, entry], [entry.resolvedModel, entry]]));
|
|
68
|
+
// Curated ORDER leads (the card's first row is the default); a curated value
|
|
69
|
+
// the endpoint no longer serves stays listed as a bare row, exactly like the
|
|
70
|
+
// card shows it.
|
|
71
|
+
const entries = opts.curated.map(value => byValue.get(value) ?? { value, label: value, source: 'configuration' });
|
|
72
|
+
const current = opts.current?.trim();
|
|
73
|
+
if (current !== undefined && current !== '' && !entries.some(entry => entry.value === current || entry.resolvedModel === current)) {
|
|
74
|
+
entries.push({ value: current, label: current, source: 'configuration' });
|
|
75
|
+
}
|
|
76
|
+
return { ...directory, entries };
|
|
77
|
+
}
|
|
42
78
|
export class CodexModelBroker {
|
|
43
79
|
deps;
|
|
44
80
|
constructor(deps) {
|
|
@@ -88,7 +124,12 @@ export class CodexModelBroker {
|
|
|
88
124
|
if (refresh)
|
|
89
125
|
await this.deps.refreshDirectory?.(home, this.deps.cwd?.(childSessionId));
|
|
90
126
|
const info = await this.modelInfo(childSessionId, childSessionId === undefined ? undefined : this.deps.ctx.localAgent.getDelegation(childSessionId)?.model);
|
|
91
|
-
|
|
127
|
+
const directory = info.directory ?? { entries: [], complete: false, customInput: true, status: 'unsupported', refreshing: false, revision: 0 };
|
|
128
|
+
return scopeServedDirectory(directory, {
|
|
129
|
+
member: childSessionId !== undefined,
|
|
130
|
+
curated: this.deps.modelList?.(home) ?? [],
|
|
131
|
+
current: info.effective,
|
|
132
|
+
});
|
|
92
133
|
}
|
|
93
134
|
async *followModelDirectory(childSessionId, signal) {
|
|
94
135
|
if (this.deps.followDirectory === undefined) {
|
package/lib/types/records.d.ts
CHANGED
|
@@ -35,10 +35,12 @@ export declare function listCodexSessions(homeDir: string): Promise<readonly Loc
|
|
|
35
35
|
* `input_tokens` is the TOTAL input including cache hits (OpenAI-style
|
|
36
36
|
* accounting, confirmed against `total_tokens` in the rollout token_count),
|
|
37
37
|
* and `cached_input_tokens` is the cache-read subset — so the uncached
|
|
38
|
-
* bucket subtracts the cached portion to avoid double counting.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* bucket subtracts the cached portion to avoid double counting.
|
|
39
|
+
* `cached_input_tokens` rides every payload (zero included), so
|
|
40
|
+
* `cacheReadTokens` is written whenever the field is present — a reader can
|
|
41
|
+
* tell "no hits" from "not reported". Codex also reports cache creation
|
|
42
|
+
* (`cache_write_input_tokens`), mapped to `cacheWriteTokens` (the shared
|
|
43
|
+
* four-bucket contract: uncached input, output, cache read, cache write).
|
|
42
44
|
* @param usage - the raw codex usage object (`turn.completed`'s `usage` or a
|
|
43
45
|
* rollout `token_count` `info.last_token_usage`/`total_token_usage`).
|
|
44
46
|
* @returns the shared usage record.
|
package/lib/types/records.js
CHANGED
|
@@ -149,10 +149,12 @@ export async function listCodexSessions(homeDir) {
|
|
|
149
149
|
* `input_tokens` is the TOTAL input including cache hits (OpenAI-style
|
|
150
150
|
* accounting, confirmed against `total_tokens` in the rollout token_count),
|
|
151
151
|
* and `cached_input_tokens` is the cache-read subset — so the uncached
|
|
152
|
-
* bucket subtracts the cached portion to avoid double counting.
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
152
|
+
* bucket subtracts the cached portion to avoid double counting.
|
|
153
|
+
* `cached_input_tokens` rides every payload (zero included), so
|
|
154
|
+
* `cacheReadTokens` is written whenever the field is present — a reader can
|
|
155
|
+
* tell "no hits" from "not reported". Codex also reports cache creation
|
|
156
|
+
* (`cache_write_input_tokens`), mapped to `cacheWriteTokens` (the shared
|
|
157
|
+
* four-bucket contract: uncached input, output, cache read, cache write).
|
|
156
158
|
* @param usage - the raw codex usage object (`turn.completed`'s `usage` or a
|
|
157
159
|
* rollout `token_count` `info.last_token_usage`/`total_token_usage`).
|
|
158
160
|
* @returns the shared usage record.
|
|
@@ -161,9 +163,12 @@ export function usageFromCodex(usage) {
|
|
|
161
163
|
const raw = usage;
|
|
162
164
|
const input = Number(raw.input_tokens);
|
|
163
165
|
const cached = Number(raw.cached_input_tokens);
|
|
166
|
+
const cacheWrite = Number(raw.cache_write_input_tokens);
|
|
164
167
|
const output = Number(raw.output_tokens);
|
|
168
|
+
// `input_tokens` already contains both cache buckets (total = input +
|
|
169
|
+
// output); the shared contract keeps the three input buckets disjoint.
|
|
165
170
|
const uncached = Number.isFinite(input) && Number.isFinite(cached)
|
|
166
|
-
? Math.max(0, input - cached)
|
|
171
|
+
? Math.max(0, input - cached - (Number.isFinite(cacheWrite) ? cacheWrite : 0))
|
|
167
172
|
: Number.isFinite(input)
|
|
168
173
|
? input
|
|
169
174
|
: 0;
|
|
@@ -171,8 +176,10 @@ export function usageFromCodex(usage) {
|
|
|
171
176
|
inputTokens: uncached,
|
|
172
177
|
outputTokens: Number.isFinite(output) ? output : 0,
|
|
173
178
|
};
|
|
174
|
-
if (Number.isFinite(cached)
|
|
179
|
+
if (Number.isFinite(cached))
|
|
175
180
|
usageRecord.cacheReadTokens = cached;
|
|
181
|
+
if (Number.isFinite(cacheWrite) && cacheWrite > 0)
|
|
182
|
+
usageRecord.cacheWriteTokens = cacheWrite;
|
|
176
183
|
return usageRecord;
|
|
177
184
|
}
|
|
178
185
|
/**
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"local-agent",
|
|
9
9
|
"codex"
|
|
10
10
|
],
|
|
11
|
-
"version": "0.2.
|
|
11
|
+
"version": "0.2.1",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"main": "lib/index.js",
|
|
14
14
|
"types": "lib/types/index.d.ts",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"dsh": ">=0.1.5-rc.1 <0.3.0-0"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@khorsheed/dsh-local-agent": "^0.2.
|
|
72
|
+
"@khorsheed/dsh-local-agent": "^0.2.1"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"@deepseek-ai/cordis": "^4.0.1",
|