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
package/dist/bot.js
CHANGED
|
@@ -70,8 +70,11 @@ const terminalAdapter_1 = require("./adapters/terminalAdapter");
|
|
|
70
70
|
const openCodeQuestionFlow_1 = require("./openCodeQuestionFlow");
|
|
71
71
|
const pendingQuestionRepost_1 = require("./pendingQuestionRepost");
|
|
72
72
|
const rateLimiter_1 = require("./rateLimiter");
|
|
73
|
+
const testTimings_1 = require("./testTimings");
|
|
73
74
|
const installManager_1 = require("./installManager");
|
|
74
75
|
const startupReadiness_1 = require("./utils/startupReadiness");
|
|
76
|
+
const doctorLine_1 = require("./utils/doctorLine");
|
|
77
|
+
const nativeWindows_1 = require("./utils/nativeWindows");
|
|
75
78
|
const state_1 = require("./state");
|
|
76
79
|
const instanceLock_1 = require("./instanceLock");
|
|
77
80
|
const shutdown_1 = require("./shutdown");
|
|
@@ -85,6 +88,7 @@ const timezoneRecompute_1 = require("./scheduler/timezoneRecompute");
|
|
|
85
88
|
const validation_1 = require("./validation");
|
|
86
89
|
const bindPicker_1 = require("./connectors/telegram/bindPicker");
|
|
87
90
|
const topicCommand_1 = require("./connectors/telegram/commands/topicCommand");
|
|
91
|
+
const forumTopicCreated_1 = require("./connectors/telegram/forumTopicCreated");
|
|
88
92
|
const providerDisconnectPlan_1 = require("./utils/providerDisconnectPlan");
|
|
89
93
|
const folderName_1 = require("./folderName");
|
|
90
94
|
const threadRouting_1 = require("./threadRouting");
|
|
@@ -138,6 +142,8 @@ const backendUnavailableError_1 = require("./adapters/backendUnavailableError");
|
|
|
138
142
|
const claudeCodeVersion_1 = require("./utils/claudeCodeVersion");
|
|
139
143
|
const keyedTransitionQueue_1 = require("./utils/keyedTransitionQueue");
|
|
140
144
|
const adoptedToolList_1 = require("./utils/adoptedToolList");
|
|
145
|
+
const lostWork_1 = require("./utils/lostWork");
|
|
146
|
+
const jsonStreamPendingWork_1 = require("./utils/jsonStreamPendingWork");
|
|
141
147
|
const compactOnIdle_1 = require("./utils/compactOnIdle");
|
|
142
148
|
const fileSendService_1 = require("./utils/fileSendService");
|
|
143
149
|
const messageSendService_1 = require("./utils/messageSendService");
|
|
@@ -319,12 +325,17 @@ function parseEnv() {
|
|
|
319
325
|
const code = e.code ?? 'unknown';
|
|
320
326
|
errors.push(`WORK_ROOT="${workRoot}" is not accessible (${code})`);
|
|
321
327
|
}
|
|
328
|
+
// A test instance's timing overrides (never a setting); an unknown key or a bad value stops the boot, naming it.
|
|
329
|
+
const testTimingsParse = (0, testTimings_1.parseTestTimings)(process.env[testTimings_1.testTimingsEnvName]);
|
|
330
|
+
if (!testTimingsParse.ok)
|
|
331
|
+
errors.push(...testTimingsParse.errors);
|
|
322
332
|
if (errors.length > 0) {
|
|
323
333
|
for (const err of errors)
|
|
324
334
|
console.error(`[startup] ${err}`);
|
|
325
335
|
process.exit(1);
|
|
326
336
|
}
|
|
327
337
|
return {
|
|
338
|
+
testTimings: testTimingsParse.ok ? testTimingsParse.timings : {},
|
|
328
339
|
botToken,
|
|
329
340
|
servedPlatforms: (0, connectorSet_1.getServedPlatforms)(connectors),
|
|
330
341
|
isTelegramServed,
|
|
@@ -336,6 +347,15 @@ function parseEnv() {
|
|
|
336
347
|
};
|
|
337
348
|
}
|
|
338
349
|
const ENV = parseEnv();
|
|
350
|
+
const testTimingsBootLine = (0, testTimings_1.formatTestTimingsBootLine)(ENV.testTimings);
|
|
351
|
+
if (testTimingsBootLine !== null)
|
|
352
|
+
console.log(testTimingsBootLine);
|
|
353
|
+
if (ENV.testTimings.telegramSendIntervalMs !== undefined)
|
|
354
|
+
(0, rateLimiter_1.configureGlobalSendInterval)(ENV.testTimings.telegramSendIntervalMs);
|
|
355
|
+
if (ENV.testTimings.backgroundHoldMaxMs !== undefined)
|
|
356
|
+
(0, createAdapter_1.registerPendingWorkHoldMs)(ENV.testTimings.backgroundHoldMaxMs);
|
|
357
|
+
/** The short retry that probes an unconfirmed usage limit (a test instance may shorten it). */
|
|
358
|
+
const limitProbeDelayMs = ENV.testTimings.limitProbeDelayMs ?? apiErrorRetry_1.unconfirmedLimitProbeDelayMs;
|
|
339
359
|
// The instance default effort (`/effort` in General) is `settings.json`'s `defaultEffort`, re-read when the file changes.
|
|
340
360
|
(0, effortLevels_1.setInstanceDefaultEffortReader)((0, instanceSettings_1.createInstanceDefaultEffortReader)(() => (0, instanceSettings_1.getInstanceSettingsPath)(), (line) => console.error(line)));
|
|
341
361
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1292,9 +1312,9 @@ function handleUnconfirmedLimit(key) {
|
|
|
1292
1312
|
return false;
|
|
1293
1313
|
}
|
|
1294
1314
|
console.log(`[apiRetry] ${k}: an unconfirmed usage limit — probing with a short retry before waiting for the reset`);
|
|
1295
|
-
void replyToThread(key, (0, i18n_1.t)('apiRetry.transientNotice', { minutes: Math.round(
|
|
1315
|
+
void replyToThread(key, (0, i18n_1.t)('apiRetry.transientNotice', { minutes: Math.round(limitProbeDelayMs / apiRetryMsPerMinute), attempt: 1 }));
|
|
1296
1316
|
const now = Date.now();
|
|
1297
|
-
armApiRetryTimer(key, { kind: 'transient', attempt: 1, fireAt: now +
|
|
1317
|
+
armApiRetryTimer(key, { kind: 'transient', attempt: 1, fireAt: now + limitProbeDelayMs, delayMs: limitProbeDelayMs, isLimitProbe: true });
|
|
1298
1318
|
return true;
|
|
1299
1319
|
}
|
|
1300
1320
|
/**
|
|
@@ -3683,6 +3703,9 @@ async function releaseThreadSession(key) {
|
|
|
3683
3703
|
* adapters can't end up polling/streaming the same thread at once — that
|
|
3684
3704
|
* desync caused mixed output, silent `/stop`, and the "I keep getting
|
|
3685
3705
|
* 'Login successful' on every message" bug reported 2026-05-15.
|
|
3706
|
+
*
|
|
3707
|
+
* Resolves the work the stop cut off that the agent's next prompt is told about (`null`: none), so a caller's line to
|
|
3708
|
+
* the operator is derived from the very record the note lists.
|
|
3686
3709
|
*/
|
|
3687
3710
|
async function switchThreadAdapter(key, newName) {
|
|
3688
3711
|
const prevName = (0, createAdapter_1.getThreadAdapterNameRaw)(key);
|
|
@@ -3691,9 +3714,13 @@ async function switchThreadAdapter(key, newName) {
|
|
|
3691
3714
|
// reminding (or, in a terminal, TYPING into) a session that never saw it.
|
|
3692
3715
|
if (prevName && !(0, createAdapter_1.checkIsSameConversationSwitch)(prevName, newName))
|
|
3693
3716
|
cancelConversationRequest(key);
|
|
3717
|
+
let switchCutOffWork = null;
|
|
3694
3718
|
if (prevName && prevName !== newName) {
|
|
3695
3719
|
try {
|
|
3696
3720
|
const prev = (0, createAdapter_1.getAdapter)(prevName);
|
|
3721
|
+
// What dies with the stopped process: its own background work and the turn it was in (an explicit stop records nothing).
|
|
3722
|
+
if (prev.checkIsActive(key))
|
|
3723
|
+
switchCutOffWork = prev.getPendingWork?.(key) ?? null;
|
|
3697
3724
|
if (prev.checkIsActive(key)) {
|
|
3698
3725
|
// `stopSession` is `void` in the AgentAdapter contract — OpenCode
|
|
3699
3726
|
// queues teardown behind its lifecycle lock and returns
|
|
@@ -3715,7 +3742,7 @@ async function switchThreadAdapter(key, newName) {
|
|
|
3715
3742
|
(0, createAdapter_1.setThreadAdapter)(key, newName);
|
|
3716
3743
|
const agent = state.getAgent(key);
|
|
3717
3744
|
if (!agent)
|
|
3718
|
-
return;
|
|
3745
|
+
return null;
|
|
3719
3746
|
// Wipe ids that don't belong to the new adapter. We persist via
|
|
3720
3747
|
// setAgent (which merges), so build a fresh record with only the
|
|
3721
3748
|
// surviving fields.
|
|
@@ -3733,13 +3760,25 @@ async function switchThreadAdapter(key, newName) {
|
|
|
3733
3760
|
// fallback switch or a plain /claude ↔ /opencode comes back to the conversation it left
|
|
3734
3761
|
// (L-D4: a sleeping conversation is woken, `/new` is the fresh start). The reattach loop never reads the parked
|
|
3735
3762
|
// slot, so the audit S12 bug (both ids on the live row) stays fixed.
|
|
3736
|
-
const
|
|
3763
|
+
const parkedBefore = state.getParkedSession(key);
|
|
3764
|
+
const switched = (0, adapterSwitchSessionIds_1.getAdapterSwitchSessionIds)({ previousName: prevName, nextName: newName, agent, parked: parkedBefore, parkedAt: new Date().toISOString() });
|
|
3737
3765
|
if (switched.ids.claudeSessionId)
|
|
3738
3766
|
next.claudeSessionId = switched.ids.claudeSessionId;
|
|
3739
3767
|
if (switched.ids.opencodeSessionId)
|
|
3740
3768
|
next.opencodeSessionId = switched.ids.opencodeSessionId;
|
|
3769
|
+
// Work the switch cut off is told twice, once to each agent: the next prompt (the hand-over bridge, the next message)
|
|
3770
|
+
// tells the agent the topic moves to, and the parked conversation keeps it for its own agent's first prompt back.
|
|
3771
|
+
const cutOffRecord = switchCutOffWork ? { ...switchCutOffWork, reason: 'switched', lostAt: Date.now() } : null;
|
|
3772
|
+
// The parked copy knows the agent the topic moves to hears it first (it may restart some of the work itself).
|
|
3773
|
+
const parkedLoss = switchCutOffWork && cutOffRecord ? { ...cutOffRecord, otherAgentTold: switchCutOffWork } : null;
|
|
3741
3774
|
if (switched.parked !== undefined)
|
|
3742
|
-
await state.setParkedSession(key, switched.parked);
|
|
3775
|
+
await state.setParkedSession(key, switched.parked && parkedLoss ? { ...switched.parked, lostWork: parkedLoss } : switched.parked);
|
|
3776
|
+
// Back to a parked conversation (the side being left may be parked in the same step): what its agent was never told
|
|
3777
|
+
// rides its first prompt.
|
|
3778
|
+
if (switched.isRestored && parkedBefore?.lostWork)
|
|
3779
|
+
await foldLostWorkIntoNextPrompt(key, parkedBefore.lostWork);
|
|
3780
|
+
if (cutOffRecord)
|
|
3781
|
+
await foldLostWorkIntoNextPrompt(key, cutOffRecord);
|
|
3743
3782
|
// The watermark is scoped to its session: it rides along while that session is kept. Every wake of a sleeping
|
|
3744
3783
|
// conversation passes through here, and the wake reads it as the proof the conversation took a turn.
|
|
3745
3784
|
if (agent.seenWatermark && [next.claudeSessionId, next.opencodeSessionId].includes(agent.seenWatermark.sessionId)) {
|
|
@@ -3749,6 +3788,7 @@ async function switchThreadAdapter(key, newName) {
|
|
|
3749
3788
|
// fields we kept.
|
|
3750
3789
|
await state.removeAgent(key);
|
|
3751
3790
|
await state.setAgent(key, next);
|
|
3791
|
+
return switchCutOffWork;
|
|
3752
3792
|
}
|
|
3753
3793
|
/**
|
|
3754
3794
|
* @description The post-start "ready" notice for an agent that just came up — or
|
|
@@ -3853,21 +3893,20 @@ async function persistSessionStart(key, adapter) {
|
|
|
3853
3893
|
}
|
|
3854
3894
|
}
|
|
3855
3895
|
/**
|
|
3856
|
-
* @description
|
|
3857
|
-
*
|
|
3858
|
-
*
|
|
3859
|
-
*
|
|
3860
|
-
*
|
|
3861
|
-
*
|
|
3862
|
-
* or began during the downtime would be misread.
|
|
3896
|
+
* @description A per-turn json-stream process that outlived the bot (L5) is stopped as soon as it is idle — its turn
|
|
3897
|
+
* (if any) ended and none of its own work is pending — the per-turn backend's after-turn stop, which the downtime
|
|
3898
|
+
* swallowed. Polled, since the turn may have ended before the adopt. The first look waits for the adopt's replay:
|
|
3899
|
+
* until the tail has consumed what stdout held at the adopt, the live state is the persisted one plus nothing, and a
|
|
3900
|
+
* turn that ended or began during the downtime would be misread. The idle-lifecycle backend has no such stop: its
|
|
3901
|
+
* adopted process, stale tool list or not, is stopped by the idle fire after the compaction.
|
|
3863
3902
|
*/
|
|
3864
|
-
function
|
|
3903
|
+
function stopAdoptedPerTurnSessionWhenIdle(key, adapter) {
|
|
3865
3904
|
const kStr = (0, sessionKey_1.keyToString)(key);
|
|
3866
3905
|
const tick = () => {
|
|
3867
3906
|
if (!adapter.checkIsActive(key))
|
|
3868
|
-
return; // gone meanwhile (a stop, a crash): nothing to
|
|
3907
|
+
return; // gone meanwhile (a stop, a crash): nothing left to stop
|
|
3869
3908
|
if (adapter.checkIsWorking?.(key)) {
|
|
3870
|
-
const timer = setTimeout(tick,
|
|
3909
|
+
const timer = setTimeout(tick, adoptedPerTurnStopPollMs);
|
|
3871
3910
|
timer.unref?.();
|
|
3872
3911
|
return;
|
|
3873
3912
|
}
|
|
@@ -3875,22 +3914,22 @@ function stopAdoptedSessionWhenIdle(key, adapter, reason) {
|
|
|
3875
3914
|
// the suspend would decline on every poll. Stop looking: a later turn's `init` reports the version,
|
|
3876
3915
|
// and the lifecycle's own stop (the idle timer, the per-turn `turnEnded`) takes over from there.
|
|
3877
3916
|
if (!adapter.checkIsAutoStopSupported(key)) {
|
|
3878
|
-
console.log(`[reattach] ${kStr}: the adopted process is kept (
|
|
3917
|
+
console.log(`[reattach] ${kStr}: the adopted process is kept (the per-turn lifecycle): its Claude Code version is unknown or too old for the auto-stop; its own lifecycle stop takes over once a turn reports it`);
|
|
3879
3918
|
return;
|
|
3880
3919
|
}
|
|
3881
|
-
console.log(`[reattach] ${kStr}: stopping the adopted process (
|
|
3920
|
+
console.log(`[reattach] ${kStr}: stopping the adopted process (the per-turn lifecycle); the next trigger resumes it`);
|
|
3882
3921
|
void suspendThreadSession(key)
|
|
3883
3922
|
.then(() => {
|
|
3884
3923
|
// The suspend declines when a turn began between the look and the transition: look again.
|
|
3885
3924
|
if (adapter.checkIsActive(key))
|
|
3886
|
-
setTimeout(tick,
|
|
3925
|
+
setTimeout(tick, adoptedPerTurnStopPollMs).unref?.();
|
|
3887
3926
|
})
|
|
3888
|
-
.catch((e) => console.warn(`[reattach] ${kStr}: the
|
|
3927
|
+
.catch((e) => console.warn(`[reattach] ${kStr}: the per-turn stop of the adopted process failed:`, e instanceof Error ? e.message : e));
|
|
3889
3928
|
};
|
|
3890
3929
|
void adapter.whenAdoptReplayed(key).then(tick);
|
|
3891
3930
|
}
|
|
3892
|
-
/** How often an adopted
|
|
3893
|
-
const
|
|
3931
|
+
/** How often an adopted per-turn process that is still working is re-checked for idleness (L5). */
|
|
3932
|
+
const adoptedPerTurnStopPollMs = 5000;
|
|
3894
3933
|
async function startAgentSession(key, args) {
|
|
3895
3934
|
const kStr = (0, sessionKey_1.keyToString)(key);
|
|
3896
3935
|
// The bound folder IS the agent's cwd — refuse to start without one. The
|
|
@@ -4159,6 +4198,8 @@ async function suspendThreadSession(key) {
|
|
|
4159
4198
|
}
|
|
4160
4199
|
autoStopUnsupportedLoggedKeys.delete(kStr);
|
|
4161
4200
|
await sessionTransitions.run(kStr, async () => {
|
|
4201
|
+
// Decide on what the process wrote up to now, not on the last tail poll: a background task's turn may have just begun.
|
|
4202
|
+
adapter.refreshWorkingState?.(key);
|
|
4162
4203
|
if (!adapter.checkIsActive(key) || adapter.checkIsWorking?.(key))
|
|
4163
4204
|
return;
|
|
4164
4205
|
startupPromptBuffer.markStarting(kStr);
|
|
@@ -4179,13 +4220,20 @@ async function suspendThreadSession(key) {
|
|
|
4179
4220
|
/** Conversations already told once that their CLI is too old for the auto-stop (L-D10). */
|
|
4180
4221
|
const autoStopUnsupportedLoggedKeys = new Set();
|
|
4181
4222
|
/**
|
|
4182
|
-
* @description `turnEnded` from a json-stream adapter
|
|
4183
|
-
*
|
|
4184
|
-
*
|
|
4185
|
-
*
|
|
4186
|
-
*
|
|
4223
|
+
* @description `turnEnded` from a json-stream adapter. The idle countdown runs from
|
|
4224
|
+
* the end of the last turn: an idle fire that found the turn still running is not
|
|
4225
|
+
* rescheduled (D2), and a turn whose answer went out through `answer_request`
|
|
4226
|
+
* produces no later output to re-arm it — the process would never reach its idle
|
|
4227
|
+
* compaction and stop, and its pending-work hold would never be enforced. (A
|
|
4228
|
+
* bot-issued compaction's own turn end is skipped by the arming's loop guard.)
|
|
4229
|
+
* In the per-turn lifecycle (L5, L-D3) the process is then stopped right after
|
|
4230
|
+
* the turn — unless a deferred compaction is armed (its drain compacts first,
|
|
4231
|
+
* and that turn ends here too) or the process still works (a background task,
|
|
4232
|
+
* input not yet taken in). The session sleeps; the next trigger resumes it with
|
|
4233
|
+
* the prompt cache warm.
|
|
4187
4234
|
*/
|
|
4188
4235
|
function handleAgentTurnEnded(key) {
|
|
4236
|
+
noteThreadActivity(key);
|
|
4189
4237
|
const adapter = (0, createAdapter_1.getThreadAdapter)(key);
|
|
4190
4238
|
if (adapter.name !== adapterNames_1.claudePerTurnAdapterName)
|
|
4191
4239
|
return;
|
|
@@ -4289,7 +4337,10 @@ async function forwardPromptToAgent(key, adapter, text, sentAtMs, options = {})
|
|
|
4289
4337
|
});
|
|
4290
4338
|
// The request header (S7) leads the per-message body, like the reply quote and
|
|
4291
4339
|
// unlike the once-per-change thread preamble: it names THIS message's request.
|
|
4292
|
-
const
|
|
4340
|
+
const messageBody = options.requestHeader ? `${options.requestHeader}${quotedBody}` : quotedBody;
|
|
4341
|
+
// A loss the agent was not told about yet (`utils/lostWork.ts`) rides the first real prompt, once, ahead of it.
|
|
4342
|
+
const lostWorkRecord = isReplayable && adapter.name !== 'terminal' ? state.getLostWork(key) : undefined;
|
|
4343
|
+
const body = lostWorkRecord ? `${(0, lostWork_1.buildLostWorkNote)(lostWorkRecord, (0, i18n_1.getLocaleEndonym)((0, i18n_1.getActiveLang)()))}\n\n${messageBody}` : messageBody;
|
|
4293
4344
|
// Cache the (reply-folded) prompt so a wedged OpenCode session can be recovered
|
|
4294
4345
|
// by restarting fresh and replaying it — replaying WITH the quote is correct.
|
|
4295
4346
|
// A genuine new prompt also opens a fresh recovery episode (clears the guard).
|
|
@@ -4341,6 +4392,9 @@ async function forwardPromptToAgent(key, adapter, text, sentAtMs, options = {})
|
|
|
4341
4392
|
await adapter.detachRunningSubagents(key);
|
|
4342
4393
|
}
|
|
4343
4394
|
adapter.sendInput(key, promptText);
|
|
4395
|
+
// Told only once the prompt carrying the note reached a live session: a forward that failed keeps it for the next.
|
|
4396
|
+
if (lostWorkRecord && adapter.checkIsActive(key))
|
|
4397
|
+
void state.clearLostWork(key);
|
|
4344
4398
|
// S2 busy-onset arm: start the Claude liveness loop the moment the prompt is
|
|
4345
4399
|
// forwarded — driven by `checkIsBusy`, not by waiting for an opportunistic
|
|
4346
4400
|
// scrape emit that may never come during a long quiet think. The arming grace
|
|
@@ -5478,13 +5532,9 @@ bot.action('open_sessions', async (ctx) => {
|
|
|
5478
5532
|
await ctx.answerCbQuery();
|
|
5479
5533
|
await handleSessionsList(key);
|
|
5480
5534
|
});
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
if (line.status === 'warn')
|
|
5485
|
-
return (0, i18n_1.t)('doctor.warn', { label: line.label, hint: line.hint ?? '' });
|
|
5486
|
-
return (0, i18n_1.t)('doctor.fail', { label: line.label, hint: line.hint ?? '' });
|
|
5487
|
-
}
|
|
5535
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5536
|
+
// /doctor — self-diagnostics (plan §20.1)
|
|
5537
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5488
5538
|
async function checkCliPresent(cmd) {
|
|
5489
5539
|
try {
|
|
5490
5540
|
await execFileAsync(cmd, ['--version'], { timeout: 1500 });
|
|
@@ -5496,6 +5546,10 @@ async function checkCliPresent(cmd) {
|
|
|
5496
5546
|
}
|
|
5497
5547
|
async function runDoctor() {
|
|
5498
5548
|
const lines = [];
|
|
5549
|
+
// 0. Native Windows runs with limits (no tmux, no self-update restart, no hot mode, no local speech install).
|
|
5550
|
+
const nativeWindowsLine = (0, nativeWindows_1.getNativeWindowsDoctorLine)(process.platform, i18n_1.t);
|
|
5551
|
+
if (nativeWindowsLine)
|
|
5552
|
+
lines.push(nativeWindowsLine);
|
|
5499
5553
|
// 1. Bot admin permissions in the configured group.
|
|
5500
5554
|
const groupId = getAllowedGroupId();
|
|
5501
5555
|
if (groupId === null) {
|
|
@@ -5602,7 +5656,7 @@ command('doctor', async (_ctx, key) => {
|
|
|
5602
5656
|
// Single big edit-in-place would be nicer, but doctor is rarely run and
|
|
5603
5657
|
// gathering the report in series keeps it dead simple.
|
|
5604
5658
|
const lines = await runDoctor();
|
|
5605
|
-
const body = lines.map(formatDoctorLine).join('\n');
|
|
5659
|
+
const body = lines.map((line) => (0, doctorLine_1.formatDoctorLine)(line, i18n_1.t)).join('\n');
|
|
5606
5660
|
await replyToThread(key, `${(0, i18n_1.t)('doctor.header')}\n\n${body}`, { parse_mode: 'Markdown' });
|
|
5607
5661
|
});
|
|
5608
5662
|
function loadMcpFile(filePath) {
|
|
@@ -5763,12 +5817,14 @@ async function applyClaudeBackendSwitch(key, target) {
|
|
|
5763
5817
|
const wasActive = (0, createAdapter_1.getThreadAdapter)(key).checkIsActive(key);
|
|
5764
5818
|
const sessionId = state.getAgent(key)?.claudeSessionId;
|
|
5765
5819
|
// Stops the previous backend, records the pick, keeps `claudeSessionId`.
|
|
5766
|
-
await switchThreadAdapter(key, target);
|
|
5820
|
+
const cutOffWork = await switchThreadAdapter(key, target);
|
|
5821
|
+
// The operator hears that the background work died with the stopped process; the agent hears it in its next prompt.
|
|
5822
|
+
const backgroundStoppedSuffix = (0, lostWork_1.checkHasLostBackgroundWork)(cutOffWork) ? `\n${(0, i18n_1.t)('fallback.backgroundStopped')}` : '';
|
|
5767
5823
|
if (!wasActive)
|
|
5768
5824
|
return (0, i18n_1.t)('claudeMode.set_idle', { label });
|
|
5769
5825
|
const decision = getWorkDirStartDecision(key);
|
|
5770
5826
|
if (!decision.ok)
|
|
5771
|
-
return decision.message
|
|
5827
|
+
return `${decision.message}${backgroundStoppedSuffix}`;
|
|
5772
5828
|
const targetAdapter = (0, createAdapter_1.getThreadAdapter)(key);
|
|
5773
5829
|
sendThreadTypingIndicator(key).catch(() => { });
|
|
5774
5830
|
if (sessionId && targetAdapter.resumeSession) {
|
|
@@ -5777,14 +5833,14 @@ async function applyClaudeBackendSwitch(key, target) {
|
|
|
5777
5833
|
try {
|
|
5778
5834
|
await targetAdapter.resumeSession(key, decision.workDir, sessionId, { isWithRecentContext: true });
|
|
5779
5835
|
await persistSessionStart(key, targetAdapter);
|
|
5780
|
-
return (0, i18n_1.t)('claudeMode.switched_resumed', { label })
|
|
5836
|
+
return `${(0, i18n_1.t)('claudeMode.switched_resumed', { label })}${backgroundStoppedSuffix}`;
|
|
5781
5837
|
}
|
|
5782
5838
|
catch (e) {
|
|
5783
5839
|
console.error('[claude_mode] resume failed, starting fresh:', e instanceof Error ? e.message : e);
|
|
5784
5840
|
}
|
|
5785
5841
|
}
|
|
5786
5842
|
const msg = await startAgentSession(key);
|
|
5787
|
-
return msg || (0, i18n_1.t)('claudeMode.switched_fresh', { label })
|
|
5843
|
+
return `${msg || (0, i18n_1.t)('claudeMode.switched_fresh', { label })}${backgroundStoppedSuffix}`;
|
|
5788
5844
|
}
|
|
5789
5845
|
command('claude_mode', async (_ctx, key, parsed) => {
|
|
5790
5846
|
if (checkIsGeneral(key)) {
|
|
@@ -6276,15 +6332,25 @@ async function switchTopicToFallbackEntry(key, from, to, options) {
|
|
|
6276
6332
|
if (from.profile !== to.profile)
|
|
6277
6333
|
await state.setClaudeProfileOverride(key, to.profile);
|
|
6278
6334
|
// The turn is dead after a limit: re-spawn now, even over background work (it is stopped; the notice says so).
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6335
|
+
lostWorkFoldingKeys.add(k);
|
|
6336
|
+
let outcome;
|
|
6337
|
+
try {
|
|
6338
|
+
outcome = await applyClaudeProfileToThread(key, { isAfterLimit: true });
|
|
6339
|
+
}
|
|
6340
|
+
finally {
|
|
6341
|
+
lostWorkFoldingKeys.delete(k);
|
|
6342
|
+
}
|
|
6282
6343
|
if (outcome === 'deferred' || outcome === 'restartRequired') {
|
|
6283
6344
|
fallbackSwitchOffers.delete(k);
|
|
6284
6345
|
const notice = (0, fallbackSwitchNotice_1.buildFallbackDeferredNotice)(from, to);
|
|
6285
6346
|
await say(notice);
|
|
6286
6347
|
return { isSwitched: false, notice };
|
|
6287
6348
|
}
|
|
6349
|
+
// The re-spawn took the old process's background work: the notice below says so, the continue nudge tells the agent.
|
|
6350
|
+
const cutOffWork = takeLostWorkOf(key);
|
|
6351
|
+
isStoppingBackground = (0, lostWork_1.checkHasLostBackgroundWork)(cutOffWork);
|
|
6352
|
+
if (cutOffWork)
|
|
6353
|
+
await foldLostWorkIntoNextPrompt(key, { ...cutOffWork, reason: 'restarted', lostAt: Date.now() });
|
|
6288
6354
|
}
|
|
6289
6355
|
else if (isSameFamily && to.backend === 'opencode') {
|
|
6290
6356
|
const error = await (0, createAdapter_1.getThreadAdapter)(key).setModel?.(key, (0, fallbackChain_1.buildOpenCodeModelLabel)(to));
|
|
@@ -6298,7 +6364,7 @@ async function switchTopicToFallbackEntry(key, from, to, options) {
|
|
|
6298
6364
|
reason: 'limit',
|
|
6299
6365
|
resetsAtText: getFallbackResetTime(from),
|
|
6300
6366
|
});
|
|
6301
|
-
await moveTopicToOtherBackend(key, to.backend, to.backend === 'claude' ? to.profile : null);
|
|
6367
|
+
isStoppingBackground = await moveTopicToOtherBackend(key, to.backend, to.backend === 'claude' ? to.profile : null);
|
|
6302
6368
|
if (to.backend === 'opencode') {
|
|
6303
6369
|
const error = await (0, createAdapter_1.getThreadAdapter)(key).setModel?.(key, (0, fallbackChain_1.buildOpenCodeModelLabel)(to));
|
|
6304
6370
|
if (error)
|
|
@@ -6325,12 +6391,13 @@ async function switchTopicToFallbackEntry(key, from, to, options) {
|
|
|
6325
6391
|
* half: record `account` as the topic's own Claude pick when one is given (the chain's rung, the operator's pick — a
|
|
6326
6392
|
* switch that only names the account the topic follows passes `null`), then `switchThreadAdapter` stops the old
|
|
6327
6393
|
* session, parks its id and brings back the target family's parked one. The context bridge is built BEFORE this, while
|
|
6328
|
-
* the adapter being left still runs.
|
|
6394
|
+
* the adapter being left still runs. Resolves whether the stop took background work with it — the caller tells the
|
|
6395
|
+
* operator (the agent is told by the next prompt, `switchThreadAdapter`).
|
|
6329
6396
|
*/
|
|
6330
6397
|
async function moveTopicToOtherBackend(key, backend, account) {
|
|
6331
6398
|
if (account !== null)
|
|
6332
6399
|
await state.setClaudeProfileOverride(key, account);
|
|
6333
|
-
await switchThreadAdapter(key, backend === 'claude' ? (0, createAdapter_1.resolveClaudeBackendName)(key) : adapterNames_1.openCodeAdapterName);
|
|
6400
|
+
return (0, lostWork_1.checkHasLostBackgroundWork)(await switchThreadAdapter(key, backend === 'claude' ? (0, createAdapter_1.resolveClaudeBackendName)(key) : adapterNames_1.openCodeAdapterName));
|
|
6334
6401
|
}
|
|
6335
6402
|
/**
|
|
6336
6403
|
* @description The second half, after a move (across backends or between accounts): start or resume the topic's
|
|
@@ -6421,7 +6488,10 @@ async function executeBackendSwitch(key, target, options) {
|
|
|
6421
6488
|
userMessage: options.userMessage,
|
|
6422
6489
|
})
|
|
6423
6490
|
: null;
|
|
6424
|
-
await moveTopicToOtherBackend(key, target.backend, target.backend === 'claude' && target.isProfilePicked ? target.profile : null);
|
|
6491
|
+
const isStoppingBackground = await moveTopicToOtherBackend(key, target.backend, target.backend === 'claude' && target.isProfilePicked ? target.profile : null);
|
|
6492
|
+
// The operator hears that the background work died with the old session; its agent hears it in the first prompt.
|
|
6493
|
+
if (isStoppingBackground)
|
|
6494
|
+
await replyToThread(key, (0, i18n_1.t)('fallback.backgroundStopped'));
|
|
6425
6495
|
cancelApiRetry(key); // a wait armed for the backend just left is over
|
|
6426
6496
|
await resumeTopicOnNewSession(key, {
|
|
6427
6497
|
modelToPin: (0, backendSwitchPlan_1.getBackendSwitchModelToPin)(target),
|
|
@@ -8080,7 +8150,11 @@ bot.on((0, filters_1.message)('forum_topic_created'), async (ctx) => {
|
|
|
8080
8150
|
// attacker-chosen folder. Stash the topic name so a later authorised-user
|
|
8081
8151
|
// message in the same thread can still benefit from fuzzy auto-bind.
|
|
8082
8152
|
const userId = ctx.from?.id;
|
|
8083
|
-
|
|
8153
|
+
const action = await (0, forumTopicCreated_1.getForumTopicCreatedAction)({ creatorId: userId, botUserId: bot.botInfo?.id, checkIsCreatorAllowed: () => checkIsAllowedUser(ctx) });
|
|
8154
|
+
// `/topic` binds and greets the topic it created itself; its own service message needs nothing more.
|
|
8155
|
+
if (action === 'ignoreOwn')
|
|
8156
|
+
return;
|
|
8157
|
+
if (action === 'cacheNameOnly') {
|
|
8084
8158
|
if (topicName) {
|
|
8085
8159
|
pendingTopicNames.set((0, sessionKey_1.keyToString)(key), { name: topicName, ts: Date.now() });
|
|
8086
8160
|
}
|
|
@@ -8490,12 +8564,9 @@ bot.action(/^reask_(\d+)$/, async (ctx) => {
|
|
|
8490
8564
|
// The label goes to the agent as a prompt and nothing else in the topic shows it whole (the buttons clip long
|
|
8491
8565
|
// labels): a label too long for the toast is posted in full, as the record of the answer that was sent.
|
|
8492
8566
|
await (0, callbackToast_1.answerCallbackWithWholeText)(ctx, label, (wholeLabel) => replyToThread(key, wholeLabel));
|
|
8493
|
-
// An answer to the agent's own question is never a request (S7), even delivered as a prompt.
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
await withThreadLocale(key, () => deliverPromptOrBuffer(key, label, true));
|
|
8497
|
-
return;
|
|
8498
|
-
}
|
|
8567
|
+
// An answer to the agent's own question is never a request (S7), even delivered as a prompt. Routed at delivery,
|
|
8568
|
+
// not on the `isStarting` read above: the stop may have ended during the toast, and a label buffered into its closed
|
|
8569
|
+
// window was never replayed.
|
|
8499
8570
|
const outcome = await withThreadLocale(key, () => deliverActivePrompt(key, adapter, label, null));
|
|
8500
8571
|
// The label reached no session (the topic was told why): the question is still open, so are its buttons.
|
|
8501
8572
|
if (outcome === 'noSession' && labels && !reAskedQuestionOptions.has((0, sessionKey_1.keyToString)(key)))
|
|
@@ -10004,6 +10075,8 @@ function handleQuestionGone(key) {
|
|
|
10004
10075
|
void unpinThreadQuestion(key);
|
|
10005
10076
|
}
|
|
10006
10077
|
function handleAgentClosed(key) {
|
|
10078
|
+
// What the process held that died with it (its own background work), taken before the cleanup below.
|
|
10079
|
+
const lostWork = takeLostWorkOf(key);
|
|
10007
10080
|
// Session is gone — drop any not-yet-sent output AND status frame so they
|
|
10008
10081
|
// don't surface after the "session ended" notice (the trailing-output bug:
|
|
10009
10082
|
// a 429 backlog could let queued deltas land seconds after the close).
|
|
@@ -10024,11 +10097,99 @@ function handleAgentClosed(key) {
|
|
|
10024
10097
|
// A closed session has nothing to compact — drop the idle watchdog + any arm.
|
|
10025
10098
|
clearThreadCompaction(key);
|
|
10026
10099
|
const adapter = (0, createAdapter_1.getThreadAdapter)(key);
|
|
10027
|
-
|
|
10100
|
+
// Work died with the process: the lost-work line (and the resume it announces) replaces the bare "session ended".
|
|
10101
|
+
if (lostWork)
|
|
10102
|
+
scheduleLostWorkNote(key, lostWork, 'exited');
|
|
10103
|
+
else
|
|
10104
|
+
replyToThread(key, (0, i18n_1.t)('agent.session_ended', { label: adapter.label })).catch(() => { });
|
|
10028
10105
|
// Banner now reads `idle`; closed sessions may also persist with the
|
|
10029
10106
|
// wrong model/agent label otherwise.
|
|
10030
10107
|
updatePinnedStatus(key).catch(() => { });
|
|
10031
10108
|
}
|
|
10109
|
+
const msPerHour = 60 * 60 * 1000;
|
|
10110
|
+
/** The pending-work hold in force, in hours, as the topic line names it (`utils/jsonStreamPendingWork.ts`; a test instance's shorter one). */
|
|
10111
|
+
const pendingWorkHoldHours = Number(((ENV.testTimings.backgroundHoldMaxMs ?? jsonStreamPendingWork_1.pendingWorkHoldDefaultMs) / msPerHour).toFixed(1));
|
|
10112
|
+
/** What the conversation's last process took with it when it ended, handed over once by its adapter. */
|
|
10113
|
+
function takeLostWorkOf(key, adapter = (0, createAdapter_1.getThreadAdapter)(key)) {
|
|
10114
|
+
return adapter.takeLostWork?.(key) ?? null;
|
|
10115
|
+
}
|
|
10116
|
+
/**
|
|
10117
|
+
* The conversations a usage-limit switch is re-spawning right now: the re-spawn's `started` leaves the loss to the
|
|
10118
|
+
* switch, which folds it into its own first prompt — decided at the call site, never by which of the two ran first.
|
|
10119
|
+
*/
|
|
10120
|
+
const lostWorkFoldingKeys = new Set();
|
|
10121
|
+
/**
|
|
10122
|
+
* @description Keep a loss for the NEXT prompt the conversation gets, with no line and no resume of its own: a move
|
|
10123
|
+
* that already says so in its notice and sends a first prompt (a limit switch's continue nudge, a backend switch's
|
|
10124
|
+
* hand-over bridge, a return to a parked conversation).
|
|
10125
|
+
*/
|
|
10126
|
+
async function foldLostWorkIntoNextPrompt(key, record) {
|
|
10127
|
+
console.log(`[lost-work] ${(0, sessionKey_1.keyToString)(key)}: the agent's own work ended with the process (${record.reason}); the move's first prompt tells the agent`);
|
|
10128
|
+
await state.setLostWork(key, (0, lostWork_1.mergeLostWorkRecords)(state.getLostWork(key), record));
|
|
10129
|
+
}
|
|
10130
|
+
/** {@link noteLostWork} from an event handler: in the conversation's locale, not awaited, a failure logged. */
|
|
10131
|
+
function scheduleLostWorkNote(key, lostWork, reason) {
|
|
10132
|
+
if (!lostWork)
|
|
10133
|
+
return;
|
|
10134
|
+
void withThreadLocale(key, () => noteLostWork(key, lostWork, reason)).catch((e) => console.warn(`[lost-work] ${(0, sessionKey_1.keyToString)(key)}:`, e instanceof Error ? e.message : e));
|
|
10135
|
+
}
|
|
10136
|
+
/**
|
|
10137
|
+
* @description A process ended with the agent's own work pending (`utils/lostWork.ts`): keep the loss until the agent
|
|
10138
|
+
* is told, post the ONE plain line in a topic, and — unless the plan says the note waits for the next prompt — resume
|
|
10139
|
+
* the conversation with the note as its prompt. Runs in the conversation's locale.
|
|
10140
|
+
*/
|
|
10141
|
+
async function noteLostWork(key, lostWork, reason) {
|
|
10142
|
+
if (!lostWork)
|
|
10143
|
+
return;
|
|
10144
|
+
const kStr = (0, sessionKey_1.keyToString)(key);
|
|
10145
|
+
const now = Date.now();
|
|
10146
|
+
const record = (0, lostWork_1.mergeLostWorkRecords)(state.getLostWork(key), { ...lostWork, reason, lostAt: now });
|
|
10147
|
+
await state.setLostWork(key, record);
|
|
10148
|
+
const lastAutoResumeAt = state.getLostWorkResumedAt(key);
|
|
10149
|
+
const isLimitWaitArmed = getArmedApiRetry(key)?.kind === 'usageLimit';
|
|
10150
|
+
const plan = (0, lostWork_1.getLostWorkPlan)({
|
|
10151
|
+
reason,
|
|
10152
|
+
isLimitWaitArmed,
|
|
10153
|
+
msSinceLastAutoResume: lastAutoResumeAt === undefined ? null : now - lastAutoResumeAt,
|
|
10154
|
+
// A tracker issue's agent answers only through a request: a bare note would have nowhere to answer.
|
|
10155
|
+
isRequestRequired: !(0, sessionKeyCodec_1.checkIsTelegramKey)(key),
|
|
10156
|
+
});
|
|
10157
|
+
console.log(`[lost-work] ${kStr}: the process ended (${reason}) with the agent's own work pending (${record.backgroundTasks.length} background task(s), ${record.scheduledJobCount} scheduled job(s)${record.hasUnreadTaskResult ? ', an unread task result' : ''}${record.wasTurnInFlight ? ', a cut-off turn' : ''}); ${plan.isAutoResume ? 'resuming it with a note' : 'the note rides the next prompt'}`);
|
|
10158
|
+
let linePlan = plan;
|
|
10159
|
+
if (plan.isAutoResume) {
|
|
10160
|
+
await state.setLostWorkResumedAt(key, now);
|
|
10161
|
+
// The line says what happened: a resume that failed leaves the note for the next prompt, worded by the same rule.
|
|
10162
|
+
if (!(await resumeConversationForLostWork(key)))
|
|
10163
|
+
linePlan = (0, lostWork_1.getHeldLostWorkPlan)({ reason, isLimitWaitArmed });
|
|
10164
|
+
}
|
|
10165
|
+
if ((0, sessionKeyCodec_1.checkIsTelegramKey)(key))
|
|
10166
|
+
replyToThread(key, (0, lostWork_1.formatLostWorkTopicLine)(record, linePlan, pendingWorkHoldHours, i18n_1.t)).catch(() => { });
|
|
10167
|
+
}
|
|
10168
|
+
/**
|
|
10169
|
+
* @description Resume the conversation with the lost-work note as its prompt; `false` when the post failed — the note
|
|
10170
|
+
* then stays for the next prompt.
|
|
10171
|
+
*/
|
|
10172
|
+
async function resumeConversationForLostWork(key) {
|
|
10173
|
+
const record = state.getLostWork(key);
|
|
10174
|
+
if (!record)
|
|
10175
|
+
return true;
|
|
10176
|
+
await state.clearLostWork(key);
|
|
10177
|
+
const posted = await (0, postToSession_1.postToSession)(createLostWorkPostDeps(), (0, sessionKey_1.keyToString)(key), (0, lostWork_1.buildLostWorkNote)(record, (0, i18n_1.getLocaleEndonym)((0, i18n_1.getActiveLang)())));
|
|
10178
|
+
if (posted.ok)
|
|
10179
|
+
return true;
|
|
10180
|
+
console.warn(`[lost-work] ${(0, sessionKey_1.keyToString)(key)}: the resume failed (${posted.reason === 'forward-failed' ? posted.error : posted.reason}); the note rides the next prompt`);
|
|
10181
|
+
await state.setLostWork(key, (0, lostWork_1.mergeLostWorkRecords)(state.getLostWork(key), record));
|
|
10182
|
+
return false;
|
|
10183
|
+
}
|
|
10184
|
+
/** The session post of a lost-work note: a bot note to the agent, not a request — no request is opened for it. */
|
|
10185
|
+
function createLostWorkPostDeps() {
|
|
10186
|
+
return {
|
|
10187
|
+
...createSessionPostDeps(),
|
|
10188
|
+
forwardPrompt: async (conversationKey, text) => {
|
|
10189
|
+
await deliverPromptOrBuffer((0, sessionKey_1.keyFromString)(conversationKey), text, startupPromptBuffer.checkIsStarting(conversationKey));
|
|
10190
|
+
},
|
|
10191
|
+
};
|
|
10192
|
+
}
|
|
10032
10193
|
function handleAgentError(key, error) {
|
|
10033
10194
|
console.error(`[Bot] adapter error ${(0, sessionKey_1.keyToString)(key)}:`, error.message);
|
|
10034
10195
|
stopClaudeLiveness(key);
|
|
@@ -10064,6 +10225,10 @@ function handleAgentStarted(key) {
|
|
|
10064
10225
|
// activity instant rather than restart at the full 55 minutes. A genuinely fresh
|
|
10065
10226
|
// session has no persisted instant and so gets the full window anyway.
|
|
10066
10227
|
rearmThreadIdleTimer(key);
|
|
10228
|
+
// A re-spawn (model, effort, account) began without the old process's background work — unless a usage-limit switch
|
|
10229
|
+
// is re-spawning it: that switch folds the loss into its own first prompt (`switchTopicToFallbackEntry`).
|
|
10230
|
+
if (!lostWorkFoldingKeys.has((0, sessionKey_1.keyToString)(key)))
|
|
10231
|
+
scheduleLostWorkNote(key, takeLostWorkOf(key), 'restarted');
|
|
10067
10232
|
}
|
|
10068
10233
|
/**
|
|
10069
10234
|
* @description `stopped` from the adapter — flip the banner back to `idle`.
|
|
@@ -10139,6 +10304,8 @@ function dropLostConversationWatermark(key) {
|
|
|
10139
10304
|
function handleAgentSuspended(key) {
|
|
10140
10305
|
clearGoneProcessFrames(key);
|
|
10141
10306
|
updatePinnedStatus(key).catch(() => { });
|
|
10307
|
+
// An idle stop takes pending work with it only once the hold ran out.
|
|
10308
|
+
scheduleLostWorkNote(key, takeLostWorkOf(key), 'holdExpired');
|
|
10142
10309
|
}
|
|
10143
10310
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
10144
10311
|
// Startup orchestration — state init, re-attach, setMyCommands, launch
|
|
@@ -10440,10 +10607,14 @@ async function reattachExistingSessions(opts = { quietReattach: false }) {
|
|
|
10440
10607
|
// json-stream thread with a persisted id but no live process SLEEPS
|
|
10441
10608
|
// (lifecycle plan L4): it is NOT re-spawned — its next trigger resumes it
|
|
10442
10609
|
// — and it gets the downtime recap read from the transcript alone. An
|
|
10443
|
-
// adopted process keeps the bot-MCP tool list it got at its own start
|
|
10444
|
-
//
|
|
10445
|
-
//
|
|
10610
|
+
// adopted process keeps the bot-MCP tool list it got at its own start: one
|
|
10611
|
+
// started under an earlier bot build is NOT stopped early — it follows the
|
|
10612
|
+
// normal idle lifecycle (compaction at the idle mark, then the stop, never
|
|
10613
|
+
// while its own work is pending) and the next trigger resumes it with the
|
|
10614
|
+
// current tools (`checkIsAdoptedToolListStale`).
|
|
10446
10615
|
const claudeJsonAdapter = (0, createAdapter_1.getAdapter)(adapterNames_1.claudeJsonStreamAdapterName);
|
|
10616
|
+
/** The conversations whose process was gone at boot with the agent's own work pending, by key — each told once. */
|
|
10617
|
+
const lostWorkFoundAtBoot = new Map();
|
|
10447
10618
|
let jsonAdopted = 0;
|
|
10448
10619
|
let jsonKilled = 0;
|
|
10449
10620
|
let jsonSleeping = 0;
|
|
@@ -10477,33 +10648,31 @@ async function reattachExistingSessions(opts = { quietReattach: false }) {
|
|
|
10477
10648
|
jsonAdopted += 1;
|
|
10478
10649
|
if (ownerAdapter.lifecycle === 'perTurn') {
|
|
10479
10650
|
// A per-turn process that outlived the bot is stopped as soon as its turn (if any) ends (L5).
|
|
10480
|
-
|
|
10651
|
+
stopAdoptedPerTurnSessionWhenIdle(key, ownerAdapter);
|
|
10481
10652
|
}
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
currentDigest: botMcpToolDigestReader?.(key.platform) ?? '',
|
|
10485
|
-
isWorking: ownerAdapter.checkIsWorking(key),
|
|
10486
|
-
});
|
|
10487
|
-
if (refresh !== 'fresh') {
|
|
10488
|
-
// Read from the persisted state (the turn-in-flight flag, the task list); the stop itself
|
|
10489
|
-
// looks again once the downtime frames have replayed.
|
|
10490
|
-
console.log(`[reattach] ${(0, sessionKey_1.keyToString)(key)}: adopted with a stale tool list (${refresh === 'stopNow' ? 'idle: stopped now' : 'working: stopped once idle'})`);
|
|
10491
|
-
if (ownerAdapter.lifecycle !== 'perTurn')
|
|
10492
|
-
stopAdoptedSessionWhenIdle(key, ownerAdapter, 'the stale tool list');
|
|
10653
|
+
if ((0, adoptedToolList_1.checkIsAdoptedToolListStale)({ persistedDigest: agent.mcpToolDigest, currentDigest: botMcpToolDigestReader?.(key.platform) ?? '' })) {
|
|
10654
|
+
console.log(`[reattach] ${(0, sessionKey_1.keyToString)(key)}: adopted with a stale tool list; it gets the current tools when it resumes after its idle stop`);
|
|
10493
10655
|
}
|
|
10494
|
-
// The surviving process keeps the account it was spawned with
|
|
10495
|
-
//
|
|
10656
|
+
// The surviving process keeps the account it was spawned with. A switch the user picked while it worked is
|
|
10657
|
+
// restored with the adopt and still applies at the end of its turn; any other change of the resolved
|
|
10658
|
+
// profile reaches it when it resumes after its idle stop.
|
|
10496
10659
|
const spawnedConfigDir = ownerAdapter.getSessionClaudeConfigDir(key);
|
|
10497
10660
|
if (spawnedConfigDir !== undefined && !(0, claudeProfiles_1.checkIsSameClaudeConfigDir)(spawnedConfigDir, resolveThreadClaudeConfigDir(key))) {
|
|
10498
10661
|
console.log(`[reattach] ${(0, sessionKey_1.keyToString)(key)}: adopted under profile dir ${spawnedConfigDir ?? 'default'}, the topic now resolves to ${resolveThreadClaudeConfigDir(key) ?? 'default'}`);
|
|
10499
|
-
if (ownerAdapter.lifecycle !== 'perTurn')
|
|
10500
|
-
stopAdoptedSessionWhenIdle(key, ownerAdapter, 'the account profile');
|
|
10501
10662
|
}
|
|
10502
10663
|
}
|
|
10503
10664
|
else {
|
|
10504
10665
|
// Dead/zombie — adopt cleaned it up itself; the thread sleeps (below)
|
|
10505
|
-
// and its next trigger resumes it from the persisted session id
|
|
10666
|
+
// and its next trigger resumes it from the persisted session id — at once
|
|
10667
|
+
// when its own work died with it (the agent is told, once the adopts are done).
|
|
10506
10668
|
jsonKilled += 1;
|
|
10669
|
+
const lostWork = takeLostWorkOf(key, ownerAdapter);
|
|
10670
|
+
if (lostWork) {
|
|
10671
|
+
lostWorkFoundAtBoot.set((0, sessionKey_1.keyToString)(key), { key, lostWork });
|
|
10672
|
+
// Told once: the record it came from drops it (the pass over the sleeping conversations below reads it too).
|
|
10673
|
+
if (agent.jsonStreamTail)
|
|
10674
|
+
void state.setJsonStreamTail(key, (0, lostWork_1.getTailRecordWithoutPendingWork)(agent.jsonStreamTail));
|
|
10675
|
+
}
|
|
10507
10676
|
}
|
|
10508
10677
|
}
|
|
10509
10678
|
catch (e) {
|
|
@@ -10525,6 +10694,13 @@ async function reattachExistingSessions(opts = { quietReattach: false }) {
|
|
|
10525
10694
|
// Sleeping (L4): no process is started — the next trigger resumes the conversation. What the agent
|
|
10526
10695
|
// produced while the bot was down is still recapped, read from the transcript on disk.
|
|
10527
10696
|
jsonSleeping += 1;
|
|
10697
|
+
// A process that died while the bot was down (its tmux session gone with it) left its pending work in the record:
|
|
10698
|
+
// the agent is told — once: the record drops it now.
|
|
10699
|
+
const goneProcessLostWork = (0, lostWork_1.getLostWorkFromTailRecord)(agent.jsonStreamTail ?? null, agent.claudeSessionId);
|
|
10700
|
+
if (agent.jsonStreamTail && (0, lostWork_1.checkHasLostWork)(goneProcessLostWork) && !lostWorkFoundAtBoot.has((0, sessionKey_1.keyToString)(key))) {
|
|
10701
|
+
lostWorkFoundAtBoot.set((0, sessionKey_1.keyToString)(key), { key, lostWork: goneProcessLostWork });
|
|
10702
|
+
void state.setJsonStreamTail(key, (0, lostWork_1.getTailRecordWithoutPendingWork)(agent.jsonStreamTail));
|
|
10703
|
+
}
|
|
10528
10704
|
// A sleeping per-turn session with a compaction due gets its idle timer from the persisted stamps (L-D7).
|
|
10529
10705
|
if (agent.name === adapterNames_1.claudePerTurnAdapterName)
|
|
10530
10706
|
rearmThreadIdleTimer(key);
|
|
@@ -10534,6 +10710,9 @@ async function reattachExistingSessions(opts = { quietReattach: false }) {
|
|
|
10534
10710
|
void postReattachRecap(key, ownerAdapter, workDirDecision.workDir, agent.claudeSessionId, agent.seenWatermark ?? null, !opts.quietReattach).catch(() => { });
|
|
10535
10711
|
}
|
|
10536
10712
|
console.log(`[reattach] claude-json-stream: adopted ${jsonAdopted}, sleeping ${jsonSleeping}, killed ${jsonKilled} orphans (quiet=${opts.quietReattach})`);
|
|
10713
|
+
// Not awaited: a resume must not hold up the rest of the boot (the prompt is buffered while the session starts).
|
|
10714
|
+
for (const { key, lostWork } of lostWorkFoundAtBoot.values())
|
|
10715
|
+
scheduleLostWorkNote(key, lostWork, 'foundDead');
|
|
10537
10716
|
// 3. Terminal — tmux shells (`term-…`). Like the Claude scan but simpler:
|
|
10538
10717
|
// a terminal has no session-id to recover, so adoption keys purely on a
|
|
10539
10718
|
// live binding whose agent is `terminal`. The tmux name is derived from
|
|
@@ -11252,10 +11431,11 @@ async function resolveBotAdminRights(groupId, botUserId) {
|
|
|
11252
11431
|
}
|
|
11253
11432
|
}
|
|
11254
11433
|
/**
|
|
11255
|
-
* @description The General topic the
|
|
11256
|
-
* General (`null` when neither exists — a Jira-only instance has no
|
|
11434
|
+
* @description The General topic the bot speaks to the operator in (the updater, the native-Windows notice): the paired group's
|
|
11435
|
+
* General when a group is paired, else the owner's DM General (`null` when neither exists — a Jira-only instance has no
|
|
11436
|
+
* Telegram to say anything in).
|
|
11257
11437
|
*/
|
|
11258
|
-
function
|
|
11438
|
+
function getOperatorGeneralKey() {
|
|
11259
11439
|
const groupId = getAllowedGroupId();
|
|
11260
11440
|
if (groupId !== null)
|
|
11261
11441
|
return (0, sessionKeyCodec_1.makeTelegramKey)(groupId, threadRouting_1.GENERAL_THREAD_ID);
|
|
@@ -11270,7 +11450,7 @@ function getUpdateGeneralKey() {
|
|
|
11270
11450
|
async function postUpdateMessageToGeneral(message) {
|
|
11271
11451
|
if (!ENV.isTelegramServed)
|
|
11272
11452
|
return;
|
|
11273
|
-
const generalKey =
|
|
11453
|
+
const generalKey = getOperatorGeneralKey();
|
|
11274
11454
|
if (generalKey === null) {
|
|
11275
11455
|
console.log(`[update] no General topic to post "${message.kind}" in`);
|
|
11276
11456
|
return;
|
|
@@ -11395,6 +11575,20 @@ async function sendStartupStatus(isHotReload) {
|
|
|
11395
11575
|
}
|
|
11396
11576
|
console.warn('[startup-status] no reachable target — status not delivered');
|
|
11397
11577
|
}
|
|
11578
|
+
/**
|
|
11579
|
+
* @description On native Windows, say once per cold start in General which features do not work there and point to WSL2,
|
|
11580
|
+
* worded in that chat's language. Best-effort like the readiness status: no General topic to speak in is not an error (the
|
|
11581
|
+
* console line already said it), and a failed send is logged by the send path.
|
|
11582
|
+
*/
|
|
11583
|
+
async function postNativeWindowsNoticeToGeneral(isHotReload) {
|
|
11584
|
+
const generalKey = getOperatorGeneralKey();
|
|
11585
|
+
if (generalKey === null)
|
|
11586
|
+
return;
|
|
11587
|
+
const text = withThreadLocale(generalKey, () => (0, nativeWindows_1.getNativeWindowsGeneralNotice)({ platform: process.platform, isHotReload }, i18n_1.t));
|
|
11588
|
+
if (text === null)
|
|
11589
|
+
return;
|
|
11590
|
+
await replyToThread(generalKey, text);
|
|
11591
|
+
}
|
|
11398
11592
|
/**
|
|
11399
11593
|
* @description Load and prepare the Jira connector, or end the start: a broken
|
|
11400
11594
|
* config, a token of another account or an unknown trigger status is fatal, with
|
|
@@ -11407,6 +11601,7 @@ async function prepareJiraConnectorOrExit() {
|
|
|
11407
11601
|
dataDir: path.dirname(state.stateFilePath),
|
|
11408
11602
|
workRoot: ENV.workRoot,
|
|
11409
11603
|
openCodeUrl: process.env.OPENCODE_URL,
|
|
11604
|
+
pollIntervalMsOverride: ENV.testTimings.jiraPollIntervalMs,
|
|
11410
11605
|
});
|
|
11411
11606
|
}
|
|
11412
11607
|
catch (error) {
|
|
@@ -11459,7 +11654,7 @@ async function startBot() {
|
|
|
11459
11654
|
// 1. State store. The `[startup] DATA_DIR=` shape is load-bearing: the
|
|
11460
11655
|
// README's two-instances troubleshooting tells the operator to compare this
|
|
11461
11656
|
// exact line across instances.
|
|
11462
|
-
state = await (0, state_1.getStateStore)();
|
|
11657
|
+
state = await (0, state_1.getStateStore)({ saveDebounceMs: ENV.testTimings.stateSaveDebounceMs });
|
|
11463
11658
|
console.log(`[startup] DATA_DIR=${path.dirname(state.stateFilePath)}`);
|
|
11464
11659
|
// R13: before anything reads, rewrites or acts on the state, it must hold only
|
|
11465
11660
|
// conversations of the platforms this instance serves.
|
|
@@ -11489,6 +11684,8 @@ async function startBot() {
|
|
|
11489
11684
|
(0, createAdapter_1.registerClaudeConfigDirReader)(resolveThreadClaudeConfigDir);
|
|
11490
11685
|
(0, createAdapter_1.registerClaudeRateLimitSink)(onClaudeRateLimitEvent);
|
|
11491
11686
|
(0, createAdapter_1.registerClaudeTurnCostSink)(onClaudeTurnCost);
|
|
11687
|
+
if (ENV.testTimings.openCodeServerUpBudgetMs !== undefined)
|
|
11688
|
+
(0, createAdapter_1.registerOpenCodeServerUpBudgetMs)(ENV.testTimings.openCodeServerUpBudgetMs);
|
|
11492
11689
|
// The usage cache: a first poll now (off the boot path), then the timer.
|
|
11493
11690
|
claudeUsageRefresher.start();
|
|
11494
11691
|
void claudeUsageRefresher.refreshAll();
|
|
@@ -11542,6 +11739,8 @@ async function startBot() {
|
|
|
11542
11739
|
return result.ok ? (result.alertRef ?? null) : null;
|
|
11543
11740
|
},
|
|
11544
11741
|
backstopMs: (0, wakeUpRules_1.getRequestBackstopMs)(process.env.REQUEST_BACKSTOP_MINUTES),
|
|
11742
|
+
sweepIntervalMs: ENV.testTimings.requestSweepIntervalMs,
|
|
11743
|
+
firstPostRetryDelayMs: ENV.testTimings.requestFirstPostRetryDelayMs,
|
|
11545
11744
|
// Only a Telegram topic in the answers-only view hides the stream from someone who expects to see the work.
|
|
11546
11745
|
checkIsAnswerTailWatched: (key) => (0, sessionKeyCodec_1.checkIsTelegramKey)(key) && !(0, topicView_1.checkIsStreamShown)(state.getDisplayPrefs(key).view),
|
|
11547
11746
|
remindAnswerTail,
|
|
@@ -11565,6 +11764,9 @@ async function startBot() {
|
|
|
11565
11764
|
const bootMode = (0, bootClassifier_1.classifyBoot)(downtimeMs);
|
|
11566
11765
|
console.log(`Boot mode: ${bootMode.isHotReload ? 'HOT RELOAD' : 'COLD START'} ` +
|
|
11567
11766
|
`(downtime=${downtimeMs === null ? 'unknown' : `${downtimeMs}ms`})`);
|
|
11767
|
+
const nativeWindowsConsoleLine = (0, nativeWindows_1.getNativeWindowsConsoleLine)(process.platform, i18n_1.t);
|
|
11768
|
+
if (nativeWindowsConsoleLine)
|
|
11769
|
+
console.warn(nativeWindowsConsoleLine);
|
|
11568
11770
|
console.log(`Chat mode: ${getChatMode()}`);
|
|
11569
11771
|
// In `both`, the DM surface only lights up when an OWNER_USER_ID is set;
|
|
11570
11772
|
// otherwise the instance serves the group only. Surface that explicitly so an
|
|
@@ -11626,10 +11828,19 @@ async function startBot() {
|
|
|
11626
11828
|
onApiError: (key, error) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'apiError', () => withThreadLocale(key, () => handleApiError(key, error))),
|
|
11627
11829
|
onNoResponse: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'noResponse', () => handleNoResponse(key)),
|
|
11628
11830
|
onQuestionGone: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'questionGone', () => withThreadLocale(key, () => handleQuestionGone(key))),
|
|
11629
|
-
onClosed: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'closed', () => withThreadLocale(key, () => handleAgentClosed(key)), () =>
|
|
11630
|
-
|
|
11831
|
+
onClosed: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'closed', () => withThreadLocale(key, () => handleAgentClosed(key)), () => {
|
|
11832
|
+
const lostWork = takeLostWorkOf(key);
|
|
11833
|
+
clearForeignSessionState(key);
|
|
11834
|
+
scheduleLostWorkNote(key, lostWork, 'exited');
|
|
11835
|
+
}),
|
|
11836
|
+
// An issue's process gets the idle countdown and the lost-work note of a re-spawn: it has no banner.
|
|
11837
|
+
onStarted: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'started', () => withThreadLocale(key, () => handleAgentStarted(key)), () => {
|
|
11838
|
+
rearmThreadIdleTimer(key);
|
|
11839
|
+
if (!lostWorkFoldingKeys.has((0, sessionKey_1.keyToString)(key)))
|
|
11840
|
+
scheduleLostWorkNote(key, takeLostWorkOf(key), 'restarted');
|
|
11841
|
+
}),
|
|
11631
11842
|
onStopped: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'stopped', () => withThreadLocale(key, () => handleAgentStopped(key)), () => clearForeignSessionState(key)),
|
|
11632
|
-
onSuspended: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'suspended', () => withThreadLocale(key, () => handleAgentSuspended(key))),
|
|
11843
|
+
onSuspended: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'suspended', () => withThreadLocale(key, () => handleAgentSuspended(key)), () => scheduleLostWorkNote(key, takeLostWorkOf(key), 'holdExpired')),
|
|
11633
11844
|
onTurnEnded: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'turnEnded', () => handleAgentTurnEnded(key)),
|
|
11634
11845
|
onContextCompacted: (key, trigger) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'contextCompacted', () => noteConversationContextReset(key, `compaction (${trigger ?? 'bot'})`)),
|
|
11635
11846
|
onConversationLost: (key) => (0, adapterEventRouting_1.dispatchAdapterEvent)(key, 'conversationLost', () => withThreadLocale(key, () => handleConversationLost(key)), () => { dropLostConversationWatermark(key); noteConversationContextReset(key, 'its transcript was lost'); }),
|
|
@@ -12073,6 +12284,12 @@ async function startBot() {
|
|
|
12073
12284
|
catch (e) {
|
|
12074
12285
|
console.warn('[startup-status] failed:', e instanceof Error ? e.message : e);
|
|
12075
12286
|
}
|
|
12287
|
+
try {
|
|
12288
|
+
await postNativeWindowsNoticeToGeneral(bootMode.isHotReload);
|
|
12289
|
+
}
|
|
12290
|
+
catch (e) {
|
|
12291
|
+
console.warn('[platform] native Windows notice failed:', e instanceof Error ? e.message : e);
|
|
12292
|
+
}
|
|
12076
12293
|
await startUpdates();
|
|
12077
12294
|
// The local speech engine: clean up after an earlier run, re-attach to / start its install. Starts no process.
|
|
12078
12295
|
void bootSttLocalEngine().catch((e) => console.warn('[STT] boot step failed:', e instanceof Error ? e.message : e));
|