@khorsheed/dsh-local-agent-claude-code 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 +27 -0
- package/README.en.md +24 -106
- package/README.i18n.yaml +2 -2
- package/README.md +24 -106
- package/lib/client.js +842 -315
- package/lib/index.js +1707 -1392
- package/lib/types/claude-cli-provider.d.ts +86 -6
- package/lib/types/claude-cli-provider.js +391 -146
- package/lib/types/client/SettingsCard.d.ts +75 -24
- package/lib/types/client/SettingsCard.js +106 -128
- package/lib/types/client/icons.d.ts +1 -1
- package/lib/types/client/index.d.ts +12 -25
- package/lib/types/client/index.js +135 -50
- package/lib/types/client/locales.d.ts +36 -24
- package/lib/types/client/locales.js +40 -28
- package/lib/types/endpoint-auth.d.ts +181 -0
- package/lib/types/endpoint-auth.js +297 -0
- package/lib/types/index.d.ts +32 -16
- package/lib/types/index.js +128 -62
- package/lib/types/live-driver.d.ts +9 -6
- package/lib/types/live-driver.js +277 -119
- package/lib/types/live-switch.d.ts +4 -1
- package/lib/types/model-broker.d.ts +4 -2
- package/lib/types/model-broker.js +63 -10
- package/lib/types/model-catalog.d.ts +35 -6
- package/lib/types/model-catalog.js +58 -68
- package/lib/types/provision.d.ts +34 -27
- package/lib/types/provision.js +49 -31
- package/lib/types/records.d.ts +4 -62
- package/lib/types/records.js +5 -382
- 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
|
@@ -12,12 +12,10 @@ import { claudeDirectory } from './model-catalog.js';
|
|
|
12
12
|
* continuable) and the same-shape push stream folded live per turn.
|
|
13
13
|
*
|
|
14
14
|
* Auth discipline (a hard requirement): the resident process runs under the
|
|
15
|
-
* family's scoped `CLAUDE_CONFIG_DIR`
|
|
16
|
-
* the
|
|
17
|
-
*
|
|
18
|
-
* never touches the user's global `~/.claude
|
|
19
|
-
* and only adds `ANTHROPIC_BASE_URL` when the plugin config supplies it (the
|
|
20
|
-
* exec path's exact env rule).
|
|
15
|
+
* family's scoped `CLAUDE_CONFIG_DIR` plus the resolved endpoint credential's
|
|
16
|
+
* `ANTHROPIC_*` variables (the scope's endpoint-auth.json over the plugin
|
|
17
|
+
* config — the exec path's exact env rule, resolved per spawn). The driver
|
|
18
|
+
* never touches the user's global `~/.claude` and never runs any `auth` verb.
|
|
21
19
|
*
|
|
22
20
|
* Protocol facts (all probed): the first stdin `user` message triggers
|
|
23
21
|
* `system/init` carrying the server-assigned session id (EVERY turn emits a
|
|
@@ -30,8 +28,9 @@ import { claudeDirectory } from './model-catalog.js';
|
|
|
30
28
|
* Fold: the resident stream speaks the exec path's exact event vocabulary
|
|
31
29
|
* (`system`/`assistant`/`user`/`result`), so each turn folds through the
|
|
32
30
|
* shared `ClaudeStreamParser` with the exec live mirror's hold-back rule (the
|
|
33
|
-
* volatile last line waits for `result`, which carries the round's usage
|
|
34
|
-
*
|
|
31
|
+
* volatile last line waits for `result`, which carries the round's usage, and
|
|
32
|
+
* an unresulted tool line holds the fold until its result merges in place —
|
|
33
|
+
* the native lifecycle folds as one closed step). Every line folds 1:1. The always-enabled
|
|
35
34
|
* `--include-partial-messages` adds incremental output on top: each `stream_event` partial accumulates into its kind's
|
|
36
35
|
* stream (native message id + block index, with ordered legacy fallback),
|
|
37
36
|
* which reserves a (turn, step) at its first delta and
|
|
@@ -62,11 +61,13 @@ import { claudeDirectory } from './model-catalog.js';
|
|
|
62
61
|
*/
|
|
63
62
|
import { ClaudeControlRequests } from './control-requests.js';
|
|
64
63
|
import { StringDecoder } from 'node:string_decoder';
|
|
64
|
+
import { randomUUID } from 'node:crypto';
|
|
65
65
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
66
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
66
67
|
import { settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
|
|
67
68
|
import { delegationEnv, persistChildSession, LiveStreamPublisher, LiveFlush, LIVE_FLUSH_INTERVAL_MS } from '@khorsheed/dsh-local-agent';
|
|
68
|
-
import { appendClaudeTranscriptLine, assistantEvent, claudeLineText, ClaudeStreamParser, claudeVersionFromInit, DEFAULT_DISPOSE_GRACE_MS, registerClaudeMemberRun, textTask, toolCallsOf, } from './claude-cli-provider.js';
|
|
69
|
-
import {
|
|
69
|
+
import { appendClaudeTranscriptLine, assistantEvent, appendClaudeRequestHeader, claudeLineText, ClaudeStreamParser, claudeVersionFromInit, DEFAULT_DISPOSE_GRACE_MS, registerClaudeMemberRun, MEMBER_TOOL_TIMEOUT_MS, textTask, toolCallsOf, usageFromClaude, } from './claude-cli-provider.js';
|
|
70
|
+
import { endpointAuthEnv } from './endpoint-auth.js';
|
|
70
71
|
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
71
72
|
export const DEFAULT_LIVE_IDLE_MS = 30 * 60_000;
|
|
72
73
|
/** Default timeout for the turn's system/init ack (a cold boot can be slow). */
|
|
@@ -81,11 +82,6 @@ const RECLAIM_EOF_GRACE_MS = 1_000;
|
|
|
81
82
|
export const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = LIVE_FLUSH_INTERVAL_MS;
|
|
82
83
|
/** Default minimum text growth between one streaming kind's snapshot messages. */
|
|
83
84
|
export const DEFAULT_SNAPSHOT_MIN_CHARS = 0;
|
|
84
|
-
/**
|
|
85
|
-
* The stream-json channel could not come up (spawn failure or the first
|
|
86
|
-
* turn's init never arrived). The provider catches exactly this and falls
|
|
87
|
-
* back to the exec one-shot; any other error is a real round failure.
|
|
88
|
-
*/
|
|
89
85
|
export class LiveChannelUnavailableError extends Error {
|
|
90
86
|
constructor(message) {
|
|
91
87
|
super(message);
|
|
@@ -419,14 +415,10 @@ export class ClaudeLiveDriver {
|
|
|
419
415
|
if (this.disposed) {
|
|
420
416
|
throw new LiveChannelUnavailableError('the live driver is disposed');
|
|
421
417
|
}
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
// next spawn). The login watch's sync alone cannot keep a resident
|
|
427
|
-
// runtime authenticatable across rotations. Best-effort: a missing grant
|
|
428
|
-
// fails the turn with the CLI's own "Not logged in", not here.
|
|
429
|
-
await syncClaudeCredentialFile(spec.homeDir, message => { this.ctx.logger.warn(message); }).catch(() => false);
|
|
418
|
+
// The endpoint credential is resolved per spawn (the scope's
|
|
419
|
+
// endpoint-auth.json over the plugin config): a key rotation reaches the
|
|
420
|
+
// resident runtime at its next respawn, with no reload and no reconcile.
|
|
421
|
+
const endpoint = await this.config.endpoint?.(spec.homeDir);
|
|
430
422
|
// The member bridge rides the spawn argv (`--mcp-config` + `--allowedTools`),
|
|
431
423
|
// exactly as in the exec path; the resident process serves one member, so
|
|
432
424
|
// its token lives with the process.
|
|
@@ -460,11 +452,14 @@ export class ClaudeLiveDriver {
|
|
|
460
452
|
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' },
|
|
461
453
|
graceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
462
454
|
// The auth discipline: exactly the exec path's env — the scoped config
|
|
463
|
-
// dir
|
|
455
|
+
// dir plus the resolved endpoint credential's `ANTHROPIC_*` variables.
|
|
464
456
|
env: delegationEnv({
|
|
465
457
|
CLAUDE_CONFIG_DIR: spec.homeDir,
|
|
458
|
+
// A bridge call may wait for the human (ask_user_question, a mode
|
|
459
|
+
// tool's approval); give MCP calls the bridge's own 30-minute bound.
|
|
460
|
+
...member === undefined ? {} : { MCP_TOOL_TIMEOUT: MEMBER_TOOL_TIMEOUT_MS },
|
|
466
461
|
...spec.configuration === undefined ? {} : { CLAUDE_CODE_EFFORT_LEVEL: spec.configuration.effort ?? 'auto' },
|
|
467
|
-
...
|
|
462
|
+
...endpointAuthEnv(endpoint),
|
|
468
463
|
}),
|
|
469
464
|
};
|
|
470
465
|
let child;
|
|
@@ -579,27 +574,22 @@ export class ClaudeLiveDriver {
|
|
|
579
574
|
*/
|
|
580
575
|
let roundModel;
|
|
581
576
|
let roundCliVersion;
|
|
582
|
-
/** Whether a folded line already
|
|
577
|
+
/** Whether a folded line already carried usage (per-call inline, or the round's total as the fallback — no backfill event exists). */
|
|
583
578
|
let usageCarried = false;
|
|
584
579
|
/**
|
|
585
|
-
* The
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
|
|
592
|
-
const reservedSteps = [];
|
|
593
|
-
/**
|
|
594
|
-
* The streaming kinds seen this round by stream key:
|
|
595
|
-
* deltas accumulate into throttled snapshot assistant/messages appended
|
|
596
|
-
* at the kind's reserved (turn, step) — the host folds repeated settles
|
|
597
|
-
* at one coordinate into one live-updating chat node, which is the only
|
|
598
|
-
* streaming channel left after 0.1.5 retired the durable per-chunk event.
|
|
599
|
-
* The kind's completed line folds at the same step and finalizes it; an
|
|
600
|
-
* entry whose completion never arrives is force-finalized at settle.
|
|
580
|
+
* The streaming blocks seen this round by stream key: deltas accumulate
|
|
581
|
+
* into throttled snapshots at the block's step once it may open in the
|
|
582
|
+
* log (previewed live before that) — the host folds repeated settles at
|
|
583
|
+
* one coordinate into one live-updating chat node. The block's completed
|
|
584
|
+
* line folds at the same step and finalizes it; an entry whose
|
|
585
|
+
* completion never arrives is force-finalized at settle. A tool that
|
|
586
|
+
* completes mid-stream folds after the stream (chronological order).
|
|
601
587
|
*/
|
|
602
588
|
const streams = new Map();
|
|
589
|
+
/** Provisional stream keys (live flush and preview identity), never log steps. */
|
|
590
|
+
let streamKeys = 0;
|
|
591
|
+
/** Line indexes already written (a line may fold ahead of one it chronologically precedes). */
|
|
592
|
+
const folded = new Set();
|
|
603
593
|
/** The stream deltas currently accumulate into (kinds stream sequentially). */
|
|
604
594
|
let activeStream;
|
|
605
595
|
let persistQueue = Promise.resolve();
|
|
@@ -635,6 +625,44 @@ export class ClaudeLiveDriver {
|
|
|
635
625
|
// key, paired with their completion in arrival order.
|
|
636
626
|
let messageKey;
|
|
637
627
|
let legacyMessage = 0;
|
|
628
|
+
// One API call per native message id. Claude emits an `assistant` event
|
|
629
|
+
// per content block, each repeating the usage snapshot taken at
|
|
630
|
+
// message_start (output counted only that far); the call's final count
|
|
631
|
+
// arrives in its message_delta. A call's lines therefore fold only once
|
|
632
|
+
// the call ended (or the round settles), and its usage — the start
|
|
633
|
+
// snapshot completed by the delta — rides its last line alone.
|
|
634
|
+
const callUsage = new Map();
|
|
635
|
+
const openCalls = new Set();
|
|
636
|
+
let currentCall;
|
|
637
|
+
const endCall = (usage) => {
|
|
638
|
+
if (currentCall === undefined)
|
|
639
|
+
return;
|
|
640
|
+
if (typeof usage === 'object' && usage !== null) {
|
|
641
|
+
const merged = { ...callUsage.get(currentCall) };
|
|
642
|
+
for (const [key, value] of Object.entries(usage))
|
|
643
|
+
if (value !== null && value !== undefined)
|
|
644
|
+
merged[key] = value;
|
|
645
|
+
callUsage.set(currentCall, merged);
|
|
646
|
+
}
|
|
647
|
+
openCalls.delete(currentCall);
|
|
648
|
+
};
|
|
649
|
+
/**
|
|
650
|
+
* A line's usage when its call is known: `skip` on every line but the
|
|
651
|
+
* call's last, the call's final usage (or, without stream events, the
|
|
652
|
+
* line's own report) on the last; `null` for an id-less line.
|
|
653
|
+
*/
|
|
654
|
+
const callLineUsage = (index) => {
|
|
655
|
+
const id = parser.lines[index]?.messageId;
|
|
656
|
+
if (id === undefined)
|
|
657
|
+
return null;
|
|
658
|
+
let last = -1;
|
|
659
|
+
parser.lines.forEach((line, position) => { if (line.messageId === id)
|
|
660
|
+
last = position; });
|
|
661
|
+
if (index !== last)
|
|
662
|
+
return { skip: true };
|
|
663
|
+
const raw = callUsage.get(id);
|
|
664
|
+
return { usage: raw !== undefined ? usageFromClaude(raw) : parser.lines[index].usage };
|
|
665
|
+
};
|
|
638
666
|
const streamKey = (kind, index) => `${messageKey ?? `legacy-${turn}-${legacyMessage}`}:${typeof index === 'number' ? index : kind}`;
|
|
639
667
|
/**
|
|
640
668
|
* Append one snapshot of a streaming kind at its reserved (turn, step).
|
|
@@ -646,6 +674,28 @@ export class ClaudeLiveDriver {
|
|
|
646
674
|
const streamPublisher = localAgent?.liveStreams === undefined ? undefined
|
|
647
675
|
: new LiveStreamPublisher(localAgent.liveStreams, childSession, turn, persist, error => this.ctx.logger.warn(`live checkpoint failed: ${String(error)}`));
|
|
648
676
|
const liveFlush = new LiveFlush(error => this.ctx.logger.warn(`live mirror flush failed: ${String(error)}`), this.config.snapshotMinIntervalMs ?? DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
|
|
677
|
+
// The step ledger. The log admits steps strictly in sequence — one open
|
|
678
|
+
// at a time, numbered when WRITTEN (`lastStep`) — while chronology is
|
|
679
|
+
// kept by start order: a stream sorts before every line that completed
|
|
680
|
+
// after its first delta (`after`). A stream's step opens in the log only
|
|
681
|
+
// once it is the oldest unfinished stream and everything before it is
|
|
682
|
+
// written; until then the browser previews it and its fold writes it.
|
|
683
|
+
let lastStep = 0;
|
|
684
|
+
let openStep;
|
|
685
|
+
const openStreamStep = (stream) => {
|
|
686
|
+
lastStep += 1;
|
|
687
|
+
childSession.append('step/start', { turn, step: lastStep });
|
|
688
|
+
stream.durable = lastStep;
|
|
689
|
+
stream.opened = true;
|
|
690
|
+
openStep = lastStep;
|
|
691
|
+
};
|
|
692
|
+
const closeOpenStep = () => {
|
|
693
|
+
if (openStep === undefined)
|
|
694
|
+
return;
|
|
695
|
+
childSession.append('step/end', { turn, step: openStep });
|
|
696
|
+
openStep = undefined;
|
|
697
|
+
};
|
|
698
|
+
const canOpenStream = (stream) => openStep === undefined && mirrored >= stream.after && streams.keys().next().value === stream.itemId;
|
|
649
699
|
const pendingStreamArrival = new Map();
|
|
650
700
|
const appendStreamSnapshot = (stream, force, interrupted, withUsage = false, final = false) => {
|
|
651
701
|
if (stream.text.trim() === '')
|
|
@@ -660,44 +710,62 @@ export class ClaudeLiveDriver {
|
|
|
660
710
|
liveFlush.cancel(stream.step);
|
|
661
711
|
const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
|
|
662
712
|
pendingStreamArrival.delete(stream.step);
|
|
663
|
-
if (
|
|
664
|
-
if (
|
|
665
|
-
|
|
666
|
-
|
|
713
|
+
if (!stream.opened) {
|
|
714
|
+
if (canOpenStream(stream))
|
|
715
|
+
openStreamStep(stream);
|
|
716
|
+
else if (!final) {
|
|
717
|
+
// Not its turn in the log yet: show it live, write it later.
|
|
718
|
+
streamPublisher?.preview(stream.step, stream.kind, stream.text, { itemId: stream.itemId, receivedAt });
|
|
719
|
+
return;
|
|
667
720
|
}
|
|
668
|
-
|
|
721
|
+
else {
|
|
722
|
+
// Settle: every line has folded; take the next step.
|
|
723
|
+
closeOpenStep();
|
|
724
|
+
openStreamStep(stream);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (streamPublisher !== undefined && !final) {
|
|
728
|
+
streamPublisher.update(stream.durable, stream.kind, stream.text, { receivedAt });
|
|
669
729
|
stream.lastSnapshotAt = now;
|
|
670
730
|
stream.lastSnapshotLen = stream.text.length;
|
|
671
731
|
return;
|
|
672
732
|
}
|
|
673
|
-
if (!stream.opened) {
|
|
674
|
-
childSession.append('step/start', { turn, step: stream.step });
|
|
675
|
-
stream.opened = true;
|
|
676
|
-
}
|
|
677
733
|
const usage = parser.usage;
|
|
734
|
+
// Per-call folds carry the accounting; the round total only ever
|
|
735
|
+
// back-fills a stream that named none (a sum would double count).
|
|
736
|
+
const totalFallback = withUsage && !usageCarried && usage !== undefined && !parser.lines.some(line => line.usage !== undefined);
|
|
678
737
|
childSession.append('assistant/message', {
|
|
679
738
|
turn,
|
|
680
|
-
step: stream.
|
|
739
|
+
step: stream.durable,
|
|
681
740
|
message: assistantEvent([
|
|
682
741
|
stream.kind === 'think'
|
|
683
742
|
? { type: 'reasoning', text: stream.text }
|
|
684
743
|
: { type: 'text', text: stream.text },
|
|
685
744
|
]),
|
|
686
745
|
stream: [],
|
|
687
|
-
...
|
|
746
|
+
...totalFallback ? { usage } : {},
|
|
688
747
|
...interrupted ? { interrupted: true } : {},
|
|
689
748
|
}, { surfaceOp: 'append' });
|
|
690
|
-
if (
|
|
749
|
+
if (totalFallback)
|
|
691
750
|
usageCarried = true;
|
|
692
751
|
stream.lastSnapshotAt = now;
|
|
693
752
|
stream.lastSnapshotLen = stream.text.length;
|
|
694
753
|
persist();
|
|
695
754
|
};
|
|
755
|
+
/** Close a stream's live presentation once its final text is in the log. */
|
|
756
|
+
const finishStream = (stream) => {
|
|
757
|
+
liveFlush.cancel(stream.step);
|
|
758
|
+
streams.delete(stream.itemId);
|
|
759
|
+
if (activeStream === stream.itemId)
|
|
760
|
+
activeStream = undefined;
|
|
761
|
+
streamPublisher?.finish(stream.step);
|
|
762
|
+
if (stream.durable !== undefined)
|
|
763
|
+
streamPublisher?.finish(stream.durable);
|
|
764
|
+
};
|
|
696
765
|
/**
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
* completion still folds at its own reserved step when it lands).
|
|
766
|
+
* Track one new streaming block: its provisional key and how many lines
|
|
767
|
+
* had completed when it began. A different block's deltas force one
|
|
768
|
+
* freshness snapshot of the current stream.
|
|
701
769
|
*/
|
|
702
770
|
const reserveStream = (itemId, kind) => {
|
|
703
771
|
if (activeStream !== undefined && activeStream !== itemId) {
|
|
@@ -711,78 +779,121 @@ export class ClaudeLiveDriver {
|
|
|
711
779
|
}
|
|
712
780
|
let stream = streams.get(itemId);
|
|
713
781
|
if (stream === undefined) {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
stream = { itemId, kind, step, text: '', lastSnapshotAt: 0, lastSnapshotLen: 0, opened: false };
|
|
782
|
+
streamKeys += 1;
|
|
783
|
+
stream = { itemId, kind, step: streamKeys, after: parser.lines.length, text: '', lastSnapshotAt: 0, lastSnapshotLen: 0, opened: false };
|
|
717
784
|
streams.set(itemId, stream);
|
|
718
785
|
}
|
|
719
786
|
activeStream = itemId;
|
|
720
787
|
};
|
|
721
|
-
/** The
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
788
|
+
/** The stream a completed line finalizes, if it was streamed. */
|
|
789
|
+
const streamOfLine = (line) => line === undefined || line.kind === 'tool' ? undefined
|
|
790
|
+
: (line.streamId === undefined ? undefined : streams.get(line.streamId))
|
|
791
|
+
?? [...streams.values()].find(candidate => candidate.kind === line.kind && (line.streamId === undefined || candidate.itemId.startsWith('legacy-')));
|
|
792
|
+
/** Write parser.lines[index] at the next step(s). */
|
|
793
|
+
const foldLine = (index, lineUsage) => {
|
|
794
|
+
const line = parser.lines[index];
|
|
795
|
+
// The fold's per-call usage (the assistant event's inline report) wins
|
|
796
|
+
// over the round-total fallback; once any line carries its own, the
|
|
797
|
+
// round total never joins the log (a sum would double count).
|
|
798
|
+
const perCall = callLineUsage(index);
|
|
799
|
+
const effectiveUsage = perCall === null ? line.usage ?? lineUsage
|
|
800
|
+
: 'skip' in perCall ? undefined : perCall.usage ?? lineUsage;
|
|
801
|
+
if (effectiveUsage !== undefined)
|
|
802
|
+
usageCarried = true;
|
|
803
|
+
const stream = streamOfLine(line);
|
|
804
|
+
if (stream !== undefined) {
|
|
805
|
+
// A streamed line finalizes its own step (opening it now if no
|
|
806
|
+
// snapshot ever could) and closes it.
|
|
807
|
+
if (!stream.opened) {
|
|
808
|
+
closeOpenStep();
|
|
809
|
+
openStreamStep(stream);
|
|
810
|
+
}
|
|
811
|
+
const blocks = stream.kind === 'think'
|
|
812
|
+
? [{ type: 'reasoning', text: line.kind === 'tool' ? '' : line.text }]
|
|
813
|
+
: [{ type: 'text', text: line.kind === 'tool' ? '' : line.text }];
|
|
814
|
+
childSession.append('assistant/message', {
|
|
815
|
+
turn,
|
|
816
|
+
step: stream.durable,
|
|
817
|
+
message: assistantEvent(blocks),
|
|
818
|
+
stream: [],
|
|
819
|
+
...effectiveUsage === undefined ? {} : { usage: effectiveUsage },
|
|
820
|
+
}, { surfaceOp: 'append' });
|
|
821
|
+
finishStream(stream);
|
|
822
|
+
closeOpenStep();
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
closeOpenStep();
|
|
826
|
+
lastStep += 1;
|
|
827
|
+
// The fold decided the usage; the line's own (per-block snapshot) must not override it.
|
|
828
|
+
const { usage: _snapshot, ...bare } = line;
|
|
829
|
+
appendClaudeTranscriptLine(childSession, turn, lastStep, bare, effectiveUsage);
|
|
729
830
|
}
|
|
730
|
-
|
|
831
|
+
folded.add(index);
|
|
832
|
+
localAgent?.reportRunProgress(childSession.id, { kind: 'delta', text: claudeLineText(line) });
|
|
731
833
|
};
|
|
732
|
-
/**
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
834
|
+
/**
|
|
835
|
+
* Mirror the completed lines before `upto` in chronological order; the
|
|
836
|
+
* last line is held back until `result`. A line waits behind every stream
|
|
837
|
+
* that began before it completed (that stream's line folds first, or the
|
|
838
|
+
* mirror stops while it still streams). A tool line whose result the
|
|
839
|
+
* stream has not recorded yet HOLDS the fold mid-round: the fold writes
|
|
840
|
+
* the native lifecycle as one closed step (the released-log reader
|
|
841
|
+
* resolves every advertised call before step/end), and the result merges
|
|
842
|
+
* into the call's line in place, so a later pass retries from here. The
|
|
843
|
+
* settle flushes (`settle: true` — the turn's result event, or the final
|
|
844
|
+
* flush after a cancel/failure) never hold: a result that will never
|
|
845
|
+
* arrive folds as the interrupted placeholder.
|
|
846
|
+
*/
|
|
847
|
+
const mirrorUpTo = (upto, withUsage, settle = false) => {
|
|
848
|
+
// Fallback carrier only: when the fold stamped per-call usage onto any
|
|
849
|
+
// line, the round total never joins the log (a sum would double count);
|
|
850
|
+
// otherwise it rides the last NON-tool line (tool/call and tool/result
|
|
851
|
+
// events carry no usage slot, and a kill mid-command ends the
|
|
852
|
+
// transcript with a tool line). A carrier mirrored in an earlier flush
|
|
853
|
+
// (before the usage was knowable) loses the accounting to the settle's
|
|
854
|
+
// final snapshot — host 0.1.5 has no usage-backfill event.
|
|
739
855
|
let usageIndex = -1;
|
|
740
|
-
if (withUsage) {
|
|
856
|
+
if (withUsage && !parser.lines.some(line => line.usage !== undefined)) {
|
|
741
857
|
for (let index = 0; index < parser.lines.length; index += 1) {
|
|
742
858
|
if (parser.lines[index]?.kind !== 'tool')
|
|
743
859
|
usageIndex = index;
|
|
744
860
|
}
|
|
745
861
|
}
|
|
746
|
-
|
|
747
|
-
const line = parser.lines[index];
|
|
748
|
-
if (line === undefined)
|
|
749
|
-
continue;
|
|
862
|
+
const usageFor = (index) => {
|
|
750
863
|
const lineUsage = withUsage && index === usageIndex ? parser.usage : undefined;
|
|
751
864
|
if (lineUsage !== undefined)
|
|
752
865
|
usageCarried = true;
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
780
|
-
else {
|
|
781
|
-
appendClaudeTranscriptLine(childSession, turn, foldStep(index), line, lineUsage);
|
|
866
|
+
return lineUsage;
|
|
867
|
+
};
|
|
868
|
+
outer: for (let index = mirrored; index < upto; index += 1) {
|
|
869
|
+
if (folded.has(index))
|
|
870
|
+
continue;
|
|
871
|
+
const line = parser.lines[index];
|
|
872
|
+
if (line === undefined)
|
|
873
|
+
continue;
|
|
874
|
+
if (!settle && line.kind === 'tool' && line.result === undefined)
|
|
875
|
+
break;
|
|
876
|
+
// A call still streaming more blocks folds once it ends (its usage is final then).
|
|
877
|
+
if (!settle && line.messageId !== undefined && openCalls.has(line.messageId))
|
|
878
|
+
break;
|
|
879
|
+
const own = streamOfLine(line);
|
|
880
|
+
// Streams order among themselves by start; a plain line follows every
|
|
881
|
+
// stream that began before it completed.
|
|
882
|
+
const all = [...streams.values()];
|
|
883
|
+
const before = own === undefined ? all.filter(stream => stream.after <= index) : all.slice(0, all.indexOf(own));
|
|
884
|
+
for (const earlier of before) {
|
|
885
|
+
const at = parser.lines.findIndex((candidate, position) => position > index && position < upto && !folded.has(position) && streamOfLine(candidate) === earlier);
|
|
886
|
+
if (at < 0)
|
|
887
|
+
break outer;
|
|
888
|
+
const atCall = parser.lines[at]?.messageId;
|
|
889
|
+
if (!settle && atCall !== undefined && openCalls.has(atCall))
|
|
890
|
+
break outer;
|
|
891
|
+
foldLine(at, usageFor(at));
|
|
782
892
|
}
|
|
783
|
-
|
|
784
|
-
localAgent?.reportRunProgress(childSession.id, { kind: 'delta', text: claudeLineText(line) });
|
|
893
|
+
foldLine(index, usageFor(index));
|
|
785
894
|
}
|
|
895
|
+
while (folded.has(mirrored))
|
|
896
|
+
mirrored += 1;
|
|
786
897
|
if (upto > 0)
|
|
787
898
|
persist();
|
|
788
899
|
};
|
|
@@ -794,6 +905,16 @@ export class ClaudeLiveDriver {
|
|
|
794
905
|
/** The round's event sink (installed on the runtime while it runs). */
|
|
795
906
|
const onEvent = (event) => {
|
|
796
907
|
const type = event['type'];
|
|
908
|
+
// Once settled, the turn/end is written: the sink stays only so a late
|
|
909
|
+
// result converges the chain. Anything else (the CLI's interrupt notice
|
|
910
|
+
// after a cancel) would land after the turn closed and corrupt the log.
|
|
911
|
+
if (roundSettled) {
|
|
912
|
+
if (type === 'result') {
|
|
913
|
+
awaitingResult = false;
|
|
914
|
+
resolveResult(event);
|
|
915
|
+
}
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
797
918
|
if (type === 'system' && typeof event['session_id'] === 'string') {
|
|
798
919
|
// EVERY turn emits an init; the first one mints the delegation id.
|
|
799
920
|
// Its model/build fields are this round's settled observation (the
|
|
@@ -811,9 +932,11 @@ export class ClaudeLiveDriver {
|
|
|
811
932
|
// The turn closes: fold the result event itself (it carries the
|
|
812
933
|
// round's usage — the fold's usage carrier rule needs it in both
|
|
813
934
|
// granularities), then flush the held-back last line WITH the usage.
|
|
935
|
+
// Settle semantics: a tool call whose result never arrived folds with
|
|
936
|
+
// the interrupted placeholder — the turn is over, so it never will.
|
|
814
937
|
awaitingResult = false;
|
|
815
938
|
parser.push(JSON.stringify(event) + '\n');
|
|
816
|
-
mirrorUpTo(parser.lines.length, true);
|
|
939
|
+
mirrorUpTo(parser.lines.length, true, true);
|
|
817
940
|
resolveResult(event);
|
|
818
941
|
return;
|
|
819
942
|
}
|
|
@@ -822,6 +945,18 @@ export class ClaudeLiveDriver {
|
|
|
822
945
|
if (inner?.['type'] === 'message_start') {
|
|
823
946
|
const message = inner['message'];
|
|
824
947
|
messageKey = typeof message?.['id'] === 'string' ? message['id'] : `legacy-${turn}-${++legacyMessage}`;
|
|
948
|
+
currentCall = typeof message?.['id'] === 'string' ? message['id'] : undefined;
|
|
949
|
+
if (currentCall !== undefined) {
|
|
950
|
+
openCalls.add(currentCall);
|
|
951
|
+
const usage = message?.['usage'];
|
|
952
|
+
if (typeof usage === 'object' && usage !== null)
|
|
953
|
+
callUsage.set(currentCall, { ...usage });
|
|
954
|
+
}
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (inner?.['type'] === 'message_delta' || inner?.['type'] === 'message_stop') {
|
|
958
|
+
endCall(inner['usage']);
|
|
959
|
+
mirrorUpTo(parser.lines.length - 1, false);
|
|
825
960
|
return;
|
|
826
961
|
}
|
|
827
962
|
const delta = inner?.['delta'];
|
|
@@ -905,6 +1040,17 @@ export class ClaudeLiveDriver {
|
|
|
905
1040
|
// The turn boundary opens before the message goes out (exec parity: the
|
|
906
1041
|
// exec path opens at spawn).
|
|
907
1042
|
childSession.append('turn/start', { turn });
|
|
1043
|
+
// The provider's own launch facts are the child log's request config —
|
|
1044
|
+
// claude's wire never carries effort, so the env the provider sets is
|
|
1045
|
+
// the authoritative source. The header contract requires a model: an
|
|
1046
|
+
// unconfigured round records nothing rather than inventing one.
|
|
1047
|
+
if (spec.configuration?.model !== undefined) {
|
|
1048
|
+
appendClaudeRequestHeader(childSession, {
|
|
1049
|
+
provider: 'claude-local',
|
|
1050
|
+
model: spec.configuration.model,
|
|
1051
|
+
reasoningEffort: (spec.configuration.effort ?? 'auto'),
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
908
1054
|
childSession.append('user/message', createUserMessage({
|
|
909
1055
|
content: [{ type: 'text', text: task }],
|
|
910
1056
|
source: { kind: 'user' },
|
|
@@ -960,7 +1106,16 @@ export class ClaudeLiveDriver {
|
|
|
960
1106
|
// held-back line (and the usage already observed) would be lost —
|
|
961
1107
|
// flush it now (the exec settle-mirror's partial-work contract).
|
|
962
1108
|
if (mirrored < parser.lines.length)
|
|
963
|
-
mirrorUpTo(parser.lines.length, true);
|
|
1109
|
+
mirrorUpTo(parser.lines.length, true, true);
|
|
1110
|
+
// A line still waiting behind a stream whose completion never came:
|
|
1111
|
+
// finalize that stream from its last snapshot, then keep folding.
|
|
1112
|
+
while (mirrored < parser.lines.length && streams.size > 0) {
|
|
1113
|
+
const oldest = streams.values().next().value;
|
|
1114
|
+
appendStreamSnapshot(oldest, true, settled.stopReason !== 'completed', false, true);
|
|
1115
|
+
finishStream(oldest);
|
|
1116
|
+
closeOpenStep();
|
|
1117
|
+
mirrorUpTo(parser.lines.length, true, true);
|
|
1118
|
+
}
|
|
964
1119
|
// Streams whose completion never arrived (abort, or the turn ended
|
|
965
1120
|
// mid-message) still finalize INSIDE the turn window: one forced
|
|
966
1121
|
// snapshot each — interrupted on a non-completed round, the LAST one
|
|
@@ -969,9 +1124,8 @@ export class ClaudeLiveDriver {
|
|
|
969
1124
|
const remaining = [...streams.values()];
|
|
970
1125
|
for (const [position, stream] of remaining.entries()) {
|
|
971
1126
|
appendStreamSnapshot(stream, true, settled.stopReason !== 'completed', position === remaining.length - 1, true);
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
childSession.append('step/end', { turn, step: stream.step });
|
|
1127
|
+
finishStream(stream);
|
|
1128
|
+
closeOpenStep();
|
|
975
1129
|
}
|
|
976
1130
|
streams.clear();
|
|
977
1131
|
activeStream = undefined;
|
|
@@ -1039,7 +1193,11 @@ export class ClaudeLiveDriver {
|
|
|
1039
1193
|
// never hold it hostage — a cancel returns immediately.
|
|
1040
1194
|
await Promise.race([accepted, abortBranch.catch(() => undefined)]);
|
|
1041
1195
|
return subprocessRunHandle({
|
|
1042
|
-
|
|
1196
|
+
// Host 0.2.1: a fresh PER-EXECUTION run id — the activation manager
|
|
1197
|
+
// rejects a run id naming a live Session (this child session is live),
|
|
1198
|
+
// and resume rounds reuse the child session id. The family's durable
|
|
1199
|
+
// tracking key (the child session id) rides the staged intent instead.
|
|
1200
|
+
id: SessionId(randomUUID()),
|
|
1043
1201
|
result,
|
|
1044
1202
|
signal: request.signal,
|
|
1045
1203
|
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 { ClaudeEndpointAuth } from './endpoint-auth.ts';
|
|
16
17
|
import { ClaudeLiveDriver } from './live-driver.ts';
|
|
17
18
|
import type { ClaudeLiveMirrorGranularity } from './live-driver.ts';
|
|
18
19
|
/** The resolved live settings (rc.1: volatile row config; 0.1.5: schema defaults ← YAML base ← user layer). */
|
|
@@ -25,8 +26,10 @@ export interface ClaudeLiveSettings {
|
|
|
25
26
|
recentModels?: readonly string[];
|
|
26
27
|
}
|
|
27
28
|
/** The Cordis-config inputs every driver generation shares. */
|
|
28
|
-
type DriverBase = Pick<Config, 'permissionMode'
|
|
29
|
+
type DriverBase = Pick<Config, 'permissionMode'> & {
|
|
29
30
|
liveIdleMs?: number;
|
|
31
|
+
/** Per-spawn endpoint credential resolver, handed to every driver generation. */
|
|
32
|
+
endpoint?: (homeDir: string) => Promise<ClaudeEndpointAuth>;
|
|
30
33
|
/**
|
|
31
34
|
* Per-spawn member model resolver, handed to every driver generation:
|
|
32
35
|
* answers the member's effective model from its session-level override, the
|
|
@@ -70,8 +70,10 @@ export interface ClaudeModelBrokerDeps {
|
|
|
70
70
|
home: string;
|
|
71
71
|
directory: (refresh?: boolean) => LocalAgentModelDirectory | Promise<LocalAgentModelDirectory>;
|
|
72
72
|
};
|
|
73
|
-
/** The settings layer: the plugin-config model, read per call. */
|
|
74
|
-
readonly settingsModel: () => string | undefined;
|
|
73
|
+
/** The settings layer: the curated list's first entry, else the plugin-config model, read per call. */
|
|
74
|
+
readonly settingsModel: (scope?: string) => string | undefined;
|
|
75
|
+
/** The scope's curated model list, read per call (empty = the fallback chain decides the vocabulary too). */
|
|
76
|
+
readonly modelList?: (scope?: string) => readonly string[];
|
|
75
77
|
/** The scoped settings.json's own model, minus live-spawn scratch writes. */
|
|
76
78
|
readonly cliDefault: (childSessionId?: string) => Promise<string | undefined>;
|
|
77
79
|
readonly directory?: (childSessionId?: string, refresh?: boolean) => LocalAgentModelDirectory | Promise<LocalAgentModelDirectory>;
|