charness 3.5.0 → 3.6.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 +38 -0
- package/DEVELOPMENT.md +12 -1
- package/README.md +16 -1
- package/SETUP.md +199 -1
- package/dist/adapters/adapterEventRouting.d.ts +2 -2
- package/dist/adapters/adapterEventRouting.d.ts.map +1 -1
- package/dist/adapters/adapterEventRouting.js +4 -3
- package/dist/adapters/adapterEventRouting.js.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.d.ts +74 -5
- package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.js +188 -9
- package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
- package/dist/adapters/createAdapter.d.ts +12 -0
- package/dist/adapters/createAdapter.d.ts.map +1 -1
- package/dist/adapters/createAdapter.js +38 -2
- package/dist/adapters/createAdapter.js.map +1 -1
- package/dist/adapters/openCodeAdapter.d.ts +5 -0
- package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/openCodeAdapter.js +7 -0
- package/dist/adapters/openCodeAdapter.js.map +1 -1
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +299 -82
- package/dist/bot.js.map +1 -1
- package/dist/connectors/jira/connector.d.ts +2 -0
- package/dist/connectors/jira/connector.d.ts.map +1 -1
- package/dist/connectors/jira/connector.js +11 -5
- package/dist/connectors/jira/connector.js.map +1 -1
- package/dist/connectors/jira/inbound.d.ts +2 -0
- package/dist/connectors/jira/inbound.d.ts.map +1 -1
- package/dist/connectors/jira/inbound.js +2 -0
- package/dist/connectors/jira/inbound.js.map +1 -1
- package/dist/connectors/jira/prompt.d.ts +4 -0
- package/dist/connectors/jira/prompt.d.ts.map +1 -1
- package/dist/connectors/jira/prompt.js +11 -2
- package/dist/connectors/jira/prompt.js.map +1 -1
- package/dist/connectors/telegram/commands/compaction.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/compaction.js +39 -8
- package/dist/connectors/telegram/commands/compaction.js.map +1 -1
- package/dist/connectors/telegram/forumTopicCreated.d.ts +20 -0
- package/dist/connectors/telegram/forumTopicCreated.d.ts.map +1 -0
- package/dist/connectors/telegram/forumTopicCreated.js +16 -0
- package/dist/connectors/telegram/forumTopicCreated.js.map +1 -0
- package/dist/i18n/de.d.ts.map +1 -1
- package/dist/i18n/de.js +14 -0
- package/dist/i18n/de.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +14 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +14 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/fr.js +14 -0
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/hi.d.ts.map +1 -1
- package/dist/i18n/hi.js +14 -0
- package/dist/i18n/hi.js.map +1 -1
- package/dist/i18n/ja.d.ts.map +1 -1
- package/dist/i18n/ja.js +14 -0
- package/dist/i18n/ja.js.map +1 -1
- package/dist/i18n/ka.d.ts.map +1 -1
- package/dist/i18n/ka.js +14 -0
- package/dist/i18n/ka.js.map +1 -1
- package/dist/i18n/pt.d.ts.map +1 -1
- package/dist/i18n/pt.js +14 -0
- package/dist/i18n/pt.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +14 -0
- package/dist/i18n/ru.js.map +1 -1
- package/dist/i18n/uk.d.ts.map +1 -1
- package/dist/i18n/uk.js +14 -0
- package/dist/i18n/uk.js.map +1 -1
- package/dist/i18n/uz.d.ts.map +1 -1
- package/dist/i18n/uz.js +14 -0
- package/dist/i18n/uz.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +14 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js.map +1 -1
- package/dist/rateLimiter.d.ts +6 -0
- package/dist/rateLimiter.d.ts.map +1 -1
- package/dist/rateLimiter.js +9 -0
- package/dist/rateLimiter.js.map +1 -1
- package/dist/requests/wakeUpEngine.d.ts +4 -0
- package/dist/requests/wakeUpEngine.d.ts.map +1 -1
- package/dist/requests/wakeUpEngine.js +3 -3
- package/dist/requests/wakeUpEngine.js.map +1 -1
- package/dist/requests/wakeUpRules.d.ts +2 -1
- package/dist/requests/wakeUpRules.d.ts.map +1 -1
- package/dist/requests/wakeUpRules.js +3 -2
- package/dist/requests/wakeUpRules.js.map +1 -1
- package/dist/scheduler/mcpSurface.js +1 -1
- package/dist/selfUpdate/updateMessages.d.ts +1 -1
- package/dist/selfUpdate/updateMessages.d.ts.map +1 -1
- package/dist/selfUpdate/updateMessages.js.map +1 -1
- package/dist/state.d.ts +36 -5
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +40 -7
- package/dist/state.js.map +1 -1
- package/dist/testTimings.d.ts +45 -0
- package/dist/testTimings.d.ts.map +1 -0
- package/dist/testTimings.js +89 -0
- package/dist/testTimings.js.map +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/adapterSwitchSessionIds.d.ts +5 -1
- package/dist/utils/adapterSwitchSessionIds.d.ts.map +1 -1
- package/dist/utils/adapterSwitchSessionIds.js +1 -1
- package/dist/utils/adapterSwitchSessionIds.js.map +1 -1
- package/dist/utils/adoptedToolList.d.ts +16 -21
- package/dist/utils/adoptedToolList.d.ts.map +1 -1
- package/dist/utils/adoptedToolList.js +18 -17
- package/dist/utils/adoptedToolList.js.map +1 -1
- package/dist/utils/claudeScheduledJobs.d.ts +49 -0
- package/dist/utils/claudeScheduledJobs.d.ts.map +1 -0
- package/dist/utils/claudeScheduledJobs.js +62 -0
- package/dist/utils/claudeScheduledJobs.js.map +1 -0
- package/dist/utils/claudeStreamJson.d.ts +31 -1
- package/dist/utils/claudeStreamJson.d.ts.map +1 -1
- package/dist/utils/claudeStreamJson.js +25 -8
- package/dist/utils/claudeStreamJson.js.map +1 -1
- package/dist/utils/compactOnIdle.d.ts +12 -0
- package/dist/utils/compactOnIdle.d.ts.map +1 -1
- package/dist/utils/compactOnIdle.js +15 -1
- package/dist/utils/compactOnIdle.js.map +1 -1
- package/dist/utils/doctorLine.d.ts +13 -0
- package/dist/utils/doctorLine.d.ts.map +1 -0
- package/dist/utils/doctorLine.js +18 -0
- package/dist/utils/doctorLine.js.map +1 -0
- package/dist/utils/effortPickerPlan.d.ts +1 -2
- package/dist/utils/effortPickerPlan.d.ts.map +1 -1
- package/dist/utils/effortPickerPlan.js.map +1 -1
- package/dist/utils/jsonStreamHost.d.ts +19 -7
- package/dist/utils/jsonStreamHost.d.ts.map +1 -1
- package/dist/utils/jsonStreamHost.js +25 -11
- package/dist/utils/jsonStreamHost.js.map +1 -1
- package/dist/utils/jsonStreamPendingWork.d.ts +44 -0
- package/dist/utils/jsonStreamPendingWork.d.ts.map +1 -0
- package/dist/utils/jsonStreamPendingWork.js +48 -0
- package/dist/utils/jsonStreamPendingWork.js.map +1 -0
- package/dist/utils/lostWork.d.ts +117 -0
- package/dist/utils/lostWork.d.ts.map +1 -0
- package/dist/utils/lostWork.js +187 -0
- package/dist/utils/lostWork.js.map +1 -0
- package/dist/utils/nativeWindows.d.ts +29 -0
- package/dist/utils/nativeWindows.d.ts.map +1 -0
- package/dist/utils/nativeWindows.js +50 -0
- package/dist/utils/nativeWindows.js.map +1 -0
- package/dist/utils/updateScreenPlan.d.ts +1 -1
- package/dist/utils/updateScreenPlan.d.ts.map +1 -1
- package/dist/utils/updateScreenPlan.js.map +1 -1
- package/package.json +2 -2
|
@@ -63,6 +63,9 @@ const claudeProfiles_1 = require("../utils/claudeProfiles");
|
|
|
63
63
|
const adapterNames_1 = require("./adapterNames");
|
|
64
64
|
const claudeCodeVersion_1 = require("../utils/claudeCodeVersion");
|
|
65
65
|
const sessionLaunchDefaults_1 = require("./sessionLaunchDefaults");
|
|
66
|
+
const jsonStreamPendingWork_1 = require("../utils/jsonStreamPendingWork");
|
|
67
|
+
const lostWork_1 = require("../utils/lostWork");
|
|
68
|
+
const claudeScheduledJobs_1 = require("../utils/claudeScheduledJobs");
|
|
66
69
|
/**
|
|
67
70
|
* In-flight explicit stops, per key — a second stop (or a start's implicit stop)
|
|
68
71
|
* AWAITS the first instead of racing it: the delayed first `tmux kill-session`
|
|
@@ -126,9 +129,12 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
126
129
|
* conversation at a time whichever lifecycle runs it.
|
|
127
130
|
*/
|
|
128
131
|
lifecycle;
|
|
132
|
+
/** How long the agent's own pending work holds the process after its last turn (`utils/jsonStreamPendingWork.ts`). */
|
|
133
|
+
pendingWorkHoldMs;
|
|
129
134
|
constructor(options = { lifecycle: 'idle' }) {
|
|
130
135
|
super();
|
|
131
136
|
this.lifecycle = options.lifecycle;
|
|
137
|
+
this.pendingWorkHoldMs = options.backgroundHoldMs ?? jsonStreamPendingWork_1.pendingWorkHoldDefaultMs;
|
|
132
138
|
this.name = options.lifecycle === 'perTurn' ? adapterNames_1.claudePerTurnAdapterName : adapterNames_1.claudeJsonStreamAdapterName;
|
|
133
139
|
this.label = options.lifecycle === 'perTurn' ? 'Claude Code (per-turn)' : 'Claude Code (stream)';
|
|
134
140
|
}
|
|
@@ -146,6 +152,12 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
146
152
|
claudeConfigDirReader = null;
|
|
147
153
|
/** The stops in flight — the module-level map shared by both lifecycles' instances (see {@link stopsInFlightByKey}). */
|
|
148
154
|
stopsInFlight = stopsInFlightByKey;
|
|
155
|
+
/** The agent's own pending work each ended process took with it, until the bot takes it ({@link takeLostWork}). */
|
|
156
|
+
lostWorkByKey = new Map();
|
|
157
|
+
/** A test instance's shorter pending-work hold (`createAdapter.registerPendingWorkHoldMs`). */
|
|
158
|
+
setPendingWorkHoldMs(holdMs) {
|
|
159
|
+
this.pendingWorkHoldMs = holdMs;
|
|
160
|
+
}
|
|
149
161
|
setDisplayPrefsReader(reader) {
|
|
150
162
|
this.displayPrefsReader = reader;
|
|
151
163
|
}
|
|
@@ -337,7 +349,8 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
337
349
|
compactionInProgress: false, pendingCompaction: null,
|
|
338
350
|
pendingQuestion: null, apiErrorFired: false, rejectedResetAtMs: null, swallowNextAbortError: false,
|
|
339
351
|
lastWatermarkOffset: -1,
|
|
340
|
-
|
|
352
|
+
backgroundTasks: new Map(), notificationTurnPendingSince: null, lastTurnEndedAt: Date.now(), isCompactionResultPending: false, scheduledJobs: new Map(),
|
|
353
|
+
claudeCodeVersion: null, applyingChunk: null,
|
|
341
354
|
adoptCatchUpOffset: null, adoptCatchUpResolvers: [],
|
|
342
355
|
};
|
|
343
356
|
this.sessions.set((0, sessionKey_1.keyToString)(key), session);
|
|
@@ -518,6 +531,12 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
518
531
|
// Only act if this session is still the current one for the key.
|
|
519
532
|
if (this.sessions.get((0, sessionKey_1.keyToString)(key)) !== session)
|
|
520
533
|
return;
|
|
534
|
+
// An explicit stop (`/quit`, `/new`, a backend switch) releases the conversation and its record: nothing to tell
|
|
535
|
+
// its agent, nothing to write back — a write after the release would bring back a record the bot dropped.
|
|
536
|
+
if (!session.isStopping || session.isSuspending) {
|
|
537
|
+
this.recordLostWork(session);
|
|
538
|
+
this.clearPendingWorkOfEndedProcess(session);
|
|
539
|
+
}
|
|
521
540
|
this.clearTimers(session);
|
|
522
541
|
session.isActive = false;
|
|
523
542
|
this.sessions.delete((0, sessionKey_1.keyToString)(key));
|
|
@@ -626,6 +645,7 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
626
645
|
const exitCode = (0, jsonStreamHost_1.readExitCodeFile)(paths.exitCodeFile);
|
|
627
646
|
if (pid === null || exitCode !== null || !(0, jsonStreamHost_1.checkIsPidAlive)(pid)) {
|
|
628
647
|
console.log(`[ClaudeJson] adopt: ${sessionName} process is dead (pid=${pid} exit=${exitCode}), cleaning up`);
|
|
648
|
+
this.storeLostWork(key, (0, lostWork_1.getLostWorkFromTailRecord)(persistedTail, claudeSessionId));
|
|
629
649
|
await this.killOrphanTmuxSession(sessionName);
|
|
630
650
|
return false;
|
|
631
651
|
}
|
|
@@ -636,7 +656,8 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
636
656
|
return false;
|
|
637
657
|
}
|
|
638
658
|
const size = (0, jsonStreamHost_1.getFileSize)(paths.stdoutFile) ?? 0;
|
|
639
|
-
const
|
|
659
|
+
const adoptedTail = (0, jsonStreamHost_1.resolveAdoptedTail)(persistedTail, claudeSessionId, size);
|
|
660
|
+
const { startOffset, isTurnInFlight, outstandingToolUseIds, claudeCodeVersion } = adoptedTail;
|
|
640
661
|
console.log(`[ClaudeJson] adopt: re-attaching to ${sessionName} in ${workDir} (pid=${pid}, tail=${startOffset}/${size})`);
|
|
641
662
|
const launchFlags = (0, jsonStreamHost_1.readWrapperScriptLaunchFlags)(paths.wrapperFile);
|
|
642
663
|
const session = {
|
|
@@ -659,7 +680,8 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
659
680
|
// the topic resolves to now, and a later re-spawn passes them on.
|
|
660
681
|
model: launchFlags.model, reportedModel: null, effort: launchFlags.effort,
|
|
661
682
|
// The account the surviving process runs on is in its own wrapper; the bot compares it with the topic's pick.
|
|
662
|
-
|
|
683
|
+
// A switch the user picked while the process worked survives the reload: it still applies at the turn's end.
|
|
684
|
+
configDir: (0, jsonStreamHost_1.readWrapperScriptConfigDir)(paths.wrapperFile), isConfigDirSwitchPending: adoptedTail.isConfigDirSwitchPending, startCostUsd: null,
|
|
663
685
|
currentResponseText: '', emittedLength: 0, outputTimer: null,
|
|
664
686
|
reasoningText: '', reasoningStartedAt: null, reasoningTimer: null, reasoningActive: false,
|
|
665
687
|
toolNamesById: new Map(), questionToolUseIds: new Set(),
|
|
@@ -670,8 +692,14 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
670
692
|
apiErrorFired: false, rejectedResetAtMs: null, swallowNextAbortError: false,
|
|
671
693
|
lastWatermarkOffset: -1,
|
|
672
694
|
// The frames that built the list and reported the version lie before the
|
|
673
|
-
// tail offset (not replayed): both come from the persisted record
|
|
674
|
-
|
|
695
|
+
// tail offset (not replayed): both come from the persisted record, with the
|
|
696
|
+
// rest of the pending work (a turn announced, the hold's start, scheduled jobs).
|
|
697
|
+
backgroundTasks: new Map(adoptedTail.backgroundTasks.map((task) => [task.taskId, task.description])),
|
|
698
|
+
notificationTurnPendingSince: adoptedTail.notificationTurnPendingSinceMs,
|
|
699
|
+
lastTurnEndedAt: adoptedTail.lastTurnEndedAtMs ?? Date.now(),
|
|
700
|
+
isCompactionResultPending: false,
|
|
701
|
+
scheduledJobs: new Map(adoptedTail.scheduledJobs.map((job) => [job.jobId, job])),
|
|
702
|
+
claudeCodeVersion, applyingChunk: null,
|
|
675
703
|
// Nothing to catch up on when the tail already sits at EOF.
|
|
676
704
|
adoptCatchUpOffset: startOffset < size ? size : null, adoptCatchUpResolvers: [],
|
|
677
705
|
};
|
|
@@ -710,6 +738,9 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
710
738
|
const resolvers = session.adoptCatchUpResolvers.splice(0);
|
|
711
739
|
for (const resolve of resolvers)
|
|
712
740
|
resolve();
|
|
741
|
+
// A switch the user picked before the reload, whose turn ended meanwhile, applies now that the state is current.
|
|
742
|
+
if (!isSessionGone)
|
|
743
|
+
this.applyPendingConfigDirSwitch(session);
|
|
713
744
|
}
|
|
714
745
|
/** Restore a pending question persisted by a previous bot life (see
|
|
715
746
|
* {@link writeQuestionSidecar}). Parse-boundary narrowing; the questions
|
|
@@ -841,15 +872,105 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
841
872
|
return false;
|
|
842
873
|
return session.isBusy || this.checkHasBackgroundWork(key);
|
|
843
874
|
}
|
|
844
|
-
/**
|
|
875
|
+
/**
|
|
876
|
+
* The agent's own pending work (background tasks and scheduled jobs within the hold, a finished task's turn on its
|
|
877
|
+
* way), input not yet taken in, a compaction in flight — "working" minus the turn (L-D2).
|
|
878
|
+
*/
|
|
845
879
|
checkHasBackgroundWork(key) {
|
|
846
880
|
const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
|
|
847
881
|
if (!session?.isActive)
|
|
848
882
|
return false;
|
|
849
|
-
return
|
|
883
|
+
return (0, jsonStreamPendingWork_1.checkHasPendingWork)(this.getPendingWorkInput(session))
|
|
850
884
|
|| session.unconsumedInputCount > 0
|
|
851
885
|
|| session.compactionInProgress;
|
|
852
886
|
}
|
|
887
|
+
/**
|
|
888
|
+
* @description The hold left for the session's background tasks / scheduled jobs (`utils/jsonStreamPendingWork.ts`):
|
|
889
|
+
* `null` without a live session or with nothing pending, `0` once spent. The idle fire re-checks no later than this.
|
|
890
|
+
*/
|
|
891
|
+
getPendingWorkHoldRemainingMs(key) {
|
|
892
|
+
const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
|
|
893
|
+
return session?.isActive ? (0, jsonStreamPendingWork_1.getPendingWorkHoldRemainingMs)(this.getPendingWorkInput(session)) : null;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* @description Read what the process wrote since the last tail poll, so the working judgement is current: a stop
|
|
897
|
+
* decides on it, and the poll may lag the process by its backoff.
|
|
898
|
+
*/
|
|
899
|
+
refreshWorkingState(key) {
|
|
900
|
+
const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
|
|
901
|
+
if (!session?.isActive)
|
|
902
|
+
return;
|
|
903
|
+
try {
|
|
904
|
+
this.drainStdoutTail(session);
|
|
905
|
+
}
|
|
906
|
+
catch (e) {
|
|
907
|
+
console.warn(`[ClaudeJson] stdout tail read failed for ${(0, sessionKey_1.keyToString)(key)}:`, e instanceof Error ? e.message : e);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* @description The agent's own pending work the conversation's LAST process took with it when it ended — a crash, a
|
|
912
|
+
* re-spawn, the idle stop after the pending-work hold, a process found gone at the adopt — handed over once (`null`
|
|
913
|
+
* when nothing was lost or it was already taken). The bot tells the agent (`utils/lostWork.ts`).
|
|
914
|
+
*/
|
|
915
|
+
takeLostWork(key) {
|
|
916
|
+
const k = (0, sessionKey_1.keyToString)(key);
|
|
917
|
+
const lostWork = this.lostWorkByKey.get(k) ?? null;
|
|
918
|
+
this.lostWorkByKey.delete(k);
|
|
919
|
+
return lostWork;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* @description What the LIVE process holds now that would die with it — read by the bot before it stops the
|
|
923
|
+
* process itself for a backend switch (an explicit stop records nothing); `null` when nothing is pending.
|
|
924
|
+
*/
|
|
925
|
+
getPendingWork(key) {
|
|
926
|
+
const session = this.sessions.get((0, sessionKey_1.keyToString)(key));
|
|
927
|
+
if (!session?.isActive)
|
|
928
|
+
return null;
|
|
929
|
+
const pendingWork = this.describePendingWork(session, { isTurnCutOff: true });
|
|
930
|
+
return (0, lostWork_1.checkHasLostWork)(pendingWork) ? pendingWork : null;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Keep what a process that ended by itself (or at the idle stop) held. A turn a crash cut off is not counted: the
|
|
934
|
+
* request core re-posts and wakes an open request (R21); the idle stop never runs mid-turn.
|
|
935
|
+
*/
|
|
936
|
+
recordLostWork(session) {
|
|
937
|
+
this.storeLostWork(session.key, this.describePendingWork(session, { isTurnCutOff: false }));
|
|
938
|
+
}
|
|
939
|
+
/** The pending work of a process: its background tasks, its scheduled jobs, a finished task's turn not yet run, and — when the bot itself cuts it off — the turn under way. */
|
|
940
|
+
describePendingWork(session, options) {
|
|
941
|
+
return {
|
|
942
|
+
backgroundTasks: [...session.backgroundTasks].map(([taskId, description]) => ({ taskId, description })),
|
|
943
|
+
scheduledJobCount: session.scheduledJobs.size,
|
|
944
|
+
hasUnreadTaskResult: (0, jsonStreamPendingWork_1.checkIsNotificationTurnPending)({ now: Date.now(), notificationTurnPendingSince: session.notificationTurnPendingSince }),
|
|
945
|
+
wasTurnInFlight: options.isTurnCutOff && session.isBusy && !session.compactionInProgress,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* The ended process's pending work is handled here (kept for the bot, or released): the record must not carry it,
|
|
950
|
+
* or the next boot would read the conversation's process as one that died with work pending.
|
|
951
|
+
*/
|
|
952
|
+
clearPendingWorkOfEndedProcess(session) {
|
|
953
|
+
session.backgroundTasks = new Map();
|
|
954
|
+
session.scheduledJobs = new Map();
|
|
955
|
+
session.notificationTurnPendingSince = null;
|
|
956
|
+
this.jsonStreamTailWriter?.(session.key, buildTailRecord(session, session.lastPersistedTailOffset));
|
|
957
|
+
}
|
|
958
|
+
storeLostWork(key, lostWork) {
|
|
959
|
+
if ((0, lostWork_1.checkHasLostWork)(lostWork))
|
|
960
|
+
this.lostWorkByKey.set((0, sessionKey_1.keyToString)(key), lostWork);
|
|
961
|
+
else
|
|
962
|
+
this.lostWorkByKey.delete((0, sessionKey_1.keyToString)(key));
|
|
963
|
+
}
|
|
964
|
+
getPendingWorkInput(session) {
|
|
965
|
+
return {
|
|
966
|
+
now: Date.now(),
|
|
967
|
+
backgroundTaskCount: session.backgroundTasks.size,
|
|
968
|
+
scheduledJobCount: session.scheduledJobs.size,
|
|
969
|
+
lastTurnEndedAt: session.lastTurnEndedAt,
|
|
970
|
+
holdMs: this.pendingWorkHoldMs,
|
|
971
|
+
notificationTurnPendingSince: session.notificationTurnPendingSince,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
853
974
|
/** The CLI version the live process reported on `system/init`, `null` until then (L-D10 gate input). */
|
|
854
975
|
getClaudeCodeVersion(key) {
|
|
855
976
|
return this.sessions.get((0, sessionKey_1.keyToString)(key))?.claudeCodeVersion ?? null;
|
|
@@ -934,6 +1055,7 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
934
1055
|
timer.unref?.();
|
|
935
1056
|
session.pendingCompaction = { resolve, timer, sawSuccess: false };
|
|
936
1057
|
session.compactionInProgress = true;
|
|
1058
|
+
session.isCompactionResultPending = true;
|
|
937
1059
|
// Fresh turn: reset the answer cursor + idle-watchdog clock, mark busy.
|
|
938
1060
|
session.currentResponseText = '';
|
|
939
1061
|
session.emittedLength = 0;
|
|
@@ -1186,6 +1308,9 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
1186
1308
|
case 'backgroundTasks':
|
|
1187
1309
|
this.handleBackgroundTasks(session, action);
|
|
1188
1310
|
return;
|
|
1311
|
+
case 'taskNotification':
|
|
1312
|
+
this.handleTaskNotification(session, action);
|
|
1313
|
+
return;
|
|
1189
1314
|
case 'apiRetry':
|
|
1190
1315
|
this.maybeEmitApiError(session, action.text);
|
|
1191
1316
|
return;
|
|
@@ -1205,10 +1330,20 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
1205
1330
|
// it `/status` and the pinned banner showed no model at all on a default
|
|
1206
1331
|
// start / resume / adopt (nothing ever set the `--model` pick).
|
|
1207
1332
|
this.handleInit(session, action);
|
|
1333
|
+
// `init` opens every turn: the turn runs from here, not from its first output.
|
|
1334
|
+
this.markTurnStarted(session);
|
|
1335
|
+
return;
|
|
1336
|
+
case 'turnStarted':
|
|
1337
|
+
this.markTurnStarted(session);
|
|
1338
|
+
if (action.wakeupSource !== null)
|
|
1339
|
+
session.scheduledJobs = (0, claudeScheduledJobs_1.getScheduledJobsAfterFiring)(session.scheduledJobs, action.wakeupSource);
|
|
1208
1340
|
return;
|
|
1209
1341
|
case 'backgroundTasks':
|
|
1210
1342
|
this.handleBackgroundTasks(session, action);
|
|
1211
1343
|
return;
|
|
1344
|
+
case 'taskNotification':
|
|
1345
|
+
this.handleTaskNotification(session, action);
|
|
1346
|
+
return;
|
|
1212
1347
|
case 'textDelta':
|
|
1213
1348
|
// Mid-turn activity marks the session busy: `sendInput` normally set it,
|
|
1214
1349
|
// but an ADOPTED session (bot restart mid-turn) reconstructs the busy
|
|
@@ -1274,7 +1409,17 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
1274
1409
|
* WHOLE on every change, so no diffing). The tail-offset persist that follows
|
|
1275
1410
|
* this frame's drain carries the new list to `state.json`. */
|
|
1276
1411
|
handleBackgroundTasks(session, action) {
|
|
1277
|
-
session.
|
|
1412
|
+
session.backgroundTasks = new Map(action.tasks.map((task) => [task.taskId, task.description]));
|
|
1413
|
+
}
|
|
1414
|
+
/** A background task ended: its own turn is coming — the list empties right after this frame, the turn shows later. */
|
|
1415
|
+
handleTaskNotification(session, action) {
|
|
1416
|
+
if (session.backgroundTasks.has(action.taskId))
|
|
1417
|
+
session.notificationTurnPendingSince = Date.now();
|
|
1418
|
+
}
|
|
1419
|
+
/** A turn began (its `init`, the CLI's running state, a scheduled job firing): busy from here; an announced turn has shown. */
|
|
1420
|
+
markTurnStarted(session) {
|
|
1421
|
+
session.isBusy = true;
|
|
1422
|
+
session.notificationTurnPendingSince = null;
|
|
1278
1423
|
}
|
|
1279
1424
|
// — compaction (F3) —
|
|
1280
1425
|
handleCompactStatus(session, action) {
|
|
@@ -1401,11 +1546,25 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
1401
1546
|
if (session.questionToolUseIds.has(action.toolUseId))
|
|
1402
1547
|
return;
|
|
1403
1548
|
const tool = session.toolNamesById.get(action.toolUseId) ?? 'tool';
|
|
1549
|
+
this.trackScheduledJob(session, tool, action.output);
|
|
1404
1550
|
if (!action.output.trim())
|
|
1405
1551
|
return;
|
|
1406
1552
|
// Mode-agnostic emit — the bot applies the per-thread `/tool_results` pref.
|
|
1407
1553
|
this.emit('toolResult', session.key, { tool, output: action.output });
|
|
1408
1554
|
}
|
|
1555
|
+
/** A `CronCreate` / `CronDelete` / `ScheduleWakeup` result changes the session-only jobs the process holds. */
|
|
1556
|
+
trackScheduledJob(session, tool, output) {
|
|
1557
|
+
if (tool === claudeScheduledJobs_1.cronCreateToolName || tool === claudeScheduledJobs_1.loopWakeupToolName) {
|
|
1558
|
+
const job = tool === claudeScheduledJobs_1.cronCreateToolName ? (0, claudeScheduledJobs_1.getScheduledJobFromCronCreateResult)(output) : (0, claudeScheduledJobs_1.getScheduledJobFromLoopWakeupResult)(output);
|
|
1559
|
+
if (job)
|
|
1560
|
+
session.scheduledJobs.set(job.jobId, job);
|
|
1561
|
+
}
|
|
1562
|
+
else if (tool === claudeScheduledJobs_1.cronDeleteToolName) {
|
|
1563
|
+
const jobId = (0, claudeScheduledJobs_1.getCancelledJobIdFromCronDeleteResult)(output);
|
|
1564
|
+
if (jobId)
|
|
1565
|
+
session.scheduledJobs.delete(jobId);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1409
1568
|
// — sub-agent (v1) —
|
|
1410
1569
|
markSubagentActive(session) {
|
|
1411
1570
|
session.subagentActive = true;
|
|
@@ -1469,6 +1628,13 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
1469
1628
|
console.log(`${exports.claudeJsonStreamUsageLogPrefix}${(0, sessionKey_1.keyToString)(session.key)}: input=${inputTokens} cacheRead=${cacheReadTokens} cacheWrite=${cacheWriteTokens} output=${outputTokens}`);
|
|
1470
1629
|
}
|
|
1471
1630
|
session.isBusy = false;
|
|
1631
|
+
// The pending-work hold counts from the agent's own last turn: a bot-issued compaction is not one. A task that
|
|
1632
|
+
// finished during the turn is NOT folded into it: its own turn starts right after this `result` (`init` +45 ms,
|
|
1633
|
+
// measured on 2.1.310), so its announcement stays until that turn shows — clearing it here would let the per-turn
|
|
1634
|
+
// stop kill the process in that gap.
|
|
1635
|
+
if (!session.isCompactionResultPending)
|
|
1636
|
+
session.lastTurnEndedAt = Date.now();
|
|
1637
|
+
session.isCompactionResultPending = false;
|
|
1472
1638
|
// Self-healing accounting: a FAILED turn (usage limit, API error) may never echo the messages that were
|
|
1473
1639
|
// queued behind it, and a count left above zero would make the session look "working" forever — a pending
|
|
1474
1640
|
// account switch would never fire. A message still genuinely queued is echoed later (the decrement clamps
|
|
@@ -1803,6 +1969,9 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
1803
1969
|
const { key, workDir, sessionId } = session;
|
|
1804
1970
|
const configDir = change.configDir === undefined ? session.configDir : change.configDir;
|
|
1805
1971
|
session.isRespawning = true;
|
|
1972
|
+
// The re-spawned process starts with none of the old one's background tasks or jobs, and the turn it cuts off is
|
|
1973
|
+
// re-prompted by nothing else.
|
|
1974
|
+
this.storeLostWork(key, this.describePendingWork(session, { isTurnCutOff: true }));
|
|
1806
1975
|
this.rejectQuestion(key);
|
|
1807
1976
|
await session.stdinWriteChain.catch(() => { });
|
|
1808
1977
|
this.clearTimers(session);
|
|
@@ -1843,6 +2012,8 @@ class ClaudeJsonStreamAdapter extends events_1.EventEmitter {
|
|
|
1843
2012
|
}
|
|
1844
2013
|
if (!options.isAfterLimit && this.checkIsWorking(key)) {
|
|
1845
2014
|
session.isConfigDirSwitchPending = true;
|
|
2015
|
+
// Recorded now: a reload before the turn ends must not drop the user's pick.
|
|
2016
|
+
this.jsonStreamTailWriter?.(key, buildTailRecord(session, session.lastPersistedTailOffset));
|
|
1846
2017
|
return 'deferred';
|
|
1847
2018
|
}
|
|
1848
2019
|
session.isConfigDirSwitchPending = false;
|
|
@@ -1900,14 +2071,22 @@ exports.ClaudeJsonStreamAdapter = ClaudeJsonStreamAdapter;
|
|
|
1900
2071
|
* state in force there (task list, turn in flight, outstanding tools) travel together
|
|
1901
2072
|
* (see `JsonStreamTailOffset`). */
|
|
1902
2073
|
function buildTailRecord(session, offsetBytes) {
|
|
2074
|
+
const backgroundTasks = [...session.backgroundTasks].map(([taskId, description]) => ({ taskId, description }));
|
|
2075
|
+
const isWorkPending = session.backgroundTasks.size > 0 || session.scheduledJobs.size > 0;
|
|
1903
2076
|
return {
|
|
1904
2077
|
sessionId: session.sessionId,
|
|
1905
2078
|
offsetBytes,
|
|
1906
|
-
backgroundTaskIds:
|
|
2079
|
+
backgroundTaskIds: backgroundTasks.map((task) => task.taskId),
|
|
1907
2080
|
isTurnInFlight: session.isBusy,
|
|
1908
2081
|
outstandingToolUseIds: [...session.outstandingToolUseIds],
|
|
1909
2082
|
// Only once known: the record never claims a version the process did not report.
|
|
1910
2083
|
...(session.claudeCodeVersion !== null ? { claudeCodeVersion: session.claudeCodeVersion } : {}),
|
|
2084
|
+
// The pending work only while there is some: an idle record stays as it was.
|
|
2085
|
+
...(backgroundTasks.length > 0 ? { backgroundTasks } : {}),
|
|
2086
|
+
...(isWorkPending ? { lastTurnEndedAtMs: session.lastTurnEndedAt } : {}),
|
|
2087
|
+
...(session.scheduledJobs.size > 0 ? { scheduledJobs: [...session.scheduledJobs.values()] } : {}),
|
|
2088
|
+
...(session.notificationTurnPendingSince !== null ? { notificationTurnPendingSinceMs: session.notificationTurnPendingSince } : {}),
|
|
2089
|
+
...(session.isConfigDirSwitchPending ? { isConfigDirSwitchPending: true } : {}),
|
|
1911
2090
|
};
|
|
1912
2091
|
}
|
|
1913
2092
|
/**
|