dsh-autorestart 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +204 -295
- package/README.zh.md +504 -0
- package/assets/icon-180.png +0 -0
- package/assets/screenshot-1.png +0 -0
- package/assets/screenshot-2.png +0 -0
- package/assets/screenshot-3.png +0 -0
- package/client.js +267 -8
- package/cordis.patch.yml +0 -0
- package/icon.svg +0 -0
- package/index.js +1284 -43
- package/lib/checkpoint.js +516 -2
- package/lib/lifecycle.js +387 -0
- package/lib/restart.js +0 -0
- package/locale/en.json +0 -0
- package/locale/zh.json +0 -0
- package/package.json +6 -3
- package/screenshots.json +5 -0
package/lib/lifecycle.js
CHANGED
|
@@ -28,6 +28,11 @@ function isFiniteNumber(value) {
|
|
|
28
28
|
function isDoneEntry(entry) {
|
|
29
29
|
if (!isObject(entry)) return true;
|
|
30
30
|
if (entry.done === true) return true;
|
|
31
|
+
// A handed-off session and an over-limit session are BOTH out of the
|
|
32
|
+
// automatic resume path for good: replaying that conversation can only fail
|
|
33
|
+
// with the same "context window exceeds limit" error.
|
|
34
|
+
if (isObject(entry.handoff)) return true;
|
|
35
|
+
if (isObject(entry.contextOverflow) && entry.contextOverflow.over === true) return true;
|
|
31
36
|
const phase = isObject(entry.goal) && typeof entry.goal.phase === 'string' ? entry.goal.phase : null;
|
|
32
37
|
return phase === 'done' || phase === 'complete' || phase === 'completed';
|
|
33
38
|
}
|
|
@@ -120,6 +125,122 @@ export function restartGate(state, { busyAgents = [], lastActivityAt = 0, now =
|
|
|
120
125
|
return { allow: true, reason: 'quiet', blockedBy: [], quietForMs };
|
|
121
126
|
}
|
|
122
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Why a session still keeps the restart waiting. `goal-active` is the only
|
|
130
|
+
* reason that can be cleared by DISARMING the goal (`ctx.goals.disarm`, which
|
|
131
|
+
* leaves the durable goal `active` but stops the round driver); every other
|
|
132
|
+
* reason is only REPORTED, because disarming would be wrong or useless:
|
|
133
|
+
* - `goal-paused` — somebody paused it (the model may not resume it),
|
|
134
|
+
* - `goal-blocked` / `goal-complete` / `goal-not-active` — not running work,
|
|
135
|
+
* - `goal-disarmed` — no continuation authority left to take away,
|
|
136
|
+
* - `goal-exhausted` — the round budget is spent, so it can never be resumed,
|
|
137
|
+
* - `goal-no-agent` — the session has no live agent to disarm through.
|
|
138
|
+
* - `goal-parked` — LEGACY: the old "this plugin already handled it" reason.
|
|
139
|
+
* It is never produced any more: the live goal state decides, not a record.
|
|
140
|
+
*/
|
|
141
|
+
export const GOAL_BLOCK_REASONS = {
|
|
142
|
+
ACTIVE: 'goal-active',
|
|
143
|
+
PARKED: 'goal-parked',
|
|
144
|
+
EXHAUSTED: 'goal-exhausted',
|
|
145
|
+
PAUSED: 'goal-paused',
|
|
146
|
+
BLOCKED: 'goal-blocked',
|
|
147
|
+
COMPLETE: 'goal-complete',
|
|
148
|
+
DISARMED: 'goal-disarmed',
|
|
149
|
+
NOT_ACTIVE: 'goal-not-active',
|
|
150
|
+
NO_AGENT: 'goal-no-agent',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Decide which busy sessions must have their goal DISARMED before a pending
|
|
155
|
+
* restart can proceed.
|
|
156
|
+
*
|
|
157
|
+
* The decision is taken on the LIVE goal view and on nothing else. A goal may
|
|
158
|
+
* be disarmed ONLY when ALL of these hold:
|
|
159
|
+
* - the goal exists,
|
|
160
|
+
* - its phase is `active`,
|
|
161
|
+
* - its activation is `armed`,
|
|
162
|
+
* - `roundsStarted < maxGoalRounds` (a goal whose round budget is spent can
|
|
163
|
+
* never be resumed, so disarming it would strand the work).
|
|
164
|
+
*
|
|
165
|
+
* The checkpoint records (`disarmedGoals` / `parkedGoals`) are REPORTING ONLY:
|
|
166
|
+
* they never veto the decision. A goal that was resumed — and is therefore
|
|
167
|
+
* armed again — must be disarmable a second time, otherwise the restart waits
|
|
168
|
+
* for a goal nobody can disarm any more (the exact deadlock this feature
|
|
169
|
+
* exists to remove). The `disarmedGoals` / `parkedGoals` options are still
|
|
170
|
+
* accepted for backwards compatibility, but they are not consulted.
|
|
171
|
+
*
|
|
172
|
+
* Disarming is safe because it only removes process-local continuation
|
|
173
|
+
* authority: the durable goal stays `active`, the round that is running right
|
|
174
|
+
* now still finishes normally, and `ctx.goals.resume(...)` re-arms it after the
|
|
175
|
+
* restart.
|
|
176
|
+
*
|
|
177
|
+
* @param {{ sessionId: string, reason?: string }[]} busyAgents
|
|
178
|
+
* @param {{
|
|
179
|
+
* getGoal?: (sessionId: string) => object|undefined,
|
|
180
|
+
* disarmedGoals?: object,
|
|
181
|
+
* parkedGoals?: object,
|
|
182
|
+
* }} [options]
|
|
183
|
+
* @returns {{
|
|
184
|
+
* disarms: { sessionId: string, goalId: string|null, revision: number|null, reason: string }[],
|
|
185
|
+
* reported: { sessionId: string, goalId: string|null, reason: string }[],
|
|
186
|
+
* }}
|
|
187
|
+
*/
|
|
188
|
+
export function selectGoalDisarms(busyAgents, { getGoal, disarmedGoals, parkedGoals } = {}) {
|
|
189
|
+
const disarms = [];
|
|
190
|
+
const reported = [];
|
|
191
|
+
const list = Array.isArray(busyAgents) ? busyAgents : [];
|
|
192
|
+
// The records are reporting material only: they must never veto a disarm.
|
|
193
|
+
void disarmedGoals;
|
|
194
|
+
void parkedGoals;
|
|
195
|
+
for (const entry of list) {
|
|
196
|
+
if (!isObject(entry)) continue;
|
|
197
|
+
const sessionId = entry.sessionId == null ? '' : String(entry.sessionId);
|
|
198
|
+
if (!sessionId) continue;
|
|
199
|
+
let goal;
|
|
200
|
+
try {
|
|
201
|
+
goal = typeof getGoal === 'function' ? getGoal(sessionId) : undefined;
|
|
202
|
+
} catch {
|
|
203
|
+
goal = undefined;
|
|
204
|
+
}
|
|
205
|
+
if (!isObject(goal)) continue;
|
|
206
|
+
const goalId = goal.id == null ? null : String(goal.id);
|
|
207
|
+
// The LIVE goal decides. A checkpoint record (this plugin disarmed the goal
|
|
208
|
+
// earlier, or the previous pause-based build parked it) is reporting material
|
|
209
|
+
// only: it must never veto a disarm, or a goal that was resumed (and is armed
|
|
210
|
+
// again) could never be disarmed a second time and the restart would wait for
|
|
211
|
+
// it forever.
|
|
212
|
+
const phase = typeof goal.phase === 'string' ? goal.phase : '';
|
|
213
|
+
if (phase !== 'active') {
|
|
214
|
+
const reason = phase === 'paused'
|
|
215
|
+
? GOAL_BLOCK_REASONS.PAUSED
|
|
216
|
+
: phase === 'blocked'
|
|
217
|
+
? GOAL_BLOCK_REASONS.BLOCKED
|
|
218
|
+
: phase === 'complete'
|
|
219
|
+
? GOAL_BLOCK_REASONS.COMPLETE
|
|
220
|
+
: GOAL_BLOCK_REASONS.NOT_ACTIVE;
|
|
221
|
+
reported.push({ sessionId, goalId, reason });
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (goal.activation !== 'armed') {
|
|
225
|
+
reported.push({ sessionId, goalId, reason: GOAL_BLOCK_REASONS.DISARMED });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const roundsStarted = isFiniteNumber(goal.roundsStarted) ? goal.roundsStarted : 0;
|
|
229
|
+
const maxGoalRounds = isFiniteNumber(goal.maxGoalRounds) ? goal.maxGoalRounds : Infinity;
|
|
230
|
+
if (roundsStarted >= maxGoalRounds) {
|
|
231
|
+
reported.push({ sessionId, goalId, reason: GOAL_BLOCK_REASONS.EXHAUSTED });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
disarms.push({
|
|
235
|
+
sessionId,
|
|
236
|
+
goalId,
|
|
237
|
+
revision: isFiniteNumber(goal.revision) ? goal.revision : null,
|
|
238
|
+
reason: GOAL_BLOCK_REASONS.ACTIVE,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return { disarms, reported };
|
|
242
|
+
}
|
|
243
|
+
|
|
123
244
|
/**
|
|
124
245
|
* The idle-aware restart policy. Call this on a poll timer (e.g. `deferPollMs`)
|
|
125
246
|
* while `state.restart` exists.
|
|
@@ -301,6 +422,272 @@ export function continuationPrompt(
|
|
|
301
422
|
].join('\n');
|
|
302
423
|
}
|
|
303
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Default character budget of the conversation tail a handoff carries.
|
|
427
|
+
*/
|
|
428
|
+
export const HANDOFF_DEFAULT_MAX_CHARS = 20000;
|
|
429
|
+
|
|
430
|
+
/** Tokens/characters of a message body, flattened to one plain-text line. */
|
|
431
|
+
function flattenText(value) {
|
|
432
|
+
if (typeof value !== 'string') return '';
|
|
433
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The tool name of one content block, or a generic label. */
|
|
437
|
+
function toolNameOf(block) {
|
|
438
|
+
const candidates = [block.name, block.toolName, block.tool, block.id];
|
|
439
|
+
if (isObject(block.data)) candidates.push(block.data.name, block.data.toolName);
|
|
440
|
+
for (const candidate of candidates) {
|
|
441
|
+
if (typeof candidate === 'string' && candidate.trim() !== '') return candidate.trim();
|
|
442
|
+
}
|
|
443
|
+
return 'unknown-tool';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Render one conversation message as plain-text lines. A text block becomes
|
|
448
|
+
* `role: text`; a tool call / tool result becomes one short line with the tool
|
|
449
|
+
* name only (tool payloads are never carried).
|
|
450
|
+
*
|
|
451
|
+
* @param {unknown} message
|
|
452
|
+
* @returns {string[]}
|
|
453
|
+
*/
|
|
454
|
+
export function renderMessageLines(message) {
|
|
455
|
+
if (!isObject(message)) return [];
|
|
456
|
+
const role = typeof message.role === 'string' && message.role !== '' ? message.role : 'message';
|
|
457
|
+
const out = [];
|
|
458
|
+
const pushText = (text) => {
|
|
459
|
+
const line = flattenText(text);
|
|
460
|
+
if (line !== '') out.push(`${role}: ${line}`);
|
|
461
|
+
};
|
|
462
|
+
const content = message.content;
|
|
463
|
+
if (typeof content === 'string') {
|
|
464
|
+
pushText(content);
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
467
|
+
if (!Array.isArray(content)) return out;
|
|
468
|
+
for (const block of content) {
|
|
469
|
+
if (typeof block === 'string') {
|
|
470
|
+
pushText(block);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (!isObject(block)) continue;
|
|
474
|
+
const type = typeof block.type === 'string' ? block.type : '';
|
|
475
|
+
if (type.includes('tool')) {
|
|
476
|
+
const label = type.includes('result') ? 'tool result' : 'tool call';
|
|
477
|
+
out.push(`${label}: ${toolNameOf(block)}`);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (typeof block.text === 'string') pushText(block.text);
|
|
481
|
+
}
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* The tail of a conversation, newest message first, bounded by a character
|
|
487
|
+
* budget. Rendering stops as soon as adding the next line would exceed the
|
|
488
|
+
* budget; one oversized single line is truncated so a handoff always carries
|
|
489
|
+
* something.
|
|
490
|
+
*
|
|
491
|
+
* @param {unknown} messages
|
|
492
|
+
* @param {{ budget?: number }} [options]
|
|
493
|
+
* @returns {{ lines: string[], chars: number }} `lines` are oldest first
|
|
494
|
+
*/
|
|
495
|
+
export function handoffTail(messages, { budget = HANDOFF_DEFAULT_MAX_CHARS } = {}) {
|
|
496
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
497
|
+
const limit = isFiniteNumber(budget) && budget > 0 ? Math.trunc(budget) : HANDOFF_DEFAULT_MAX_CHARS;
|
|
498
|
+
const picked = [];
|
|
499
|
+
let chars = 0;
|
|
500
|
+
for (let i = list.length - 1; i >= 0; i -= 1) {
|
|
501
|
+
const lines = renderMessageLines(list[i]);
|
|
502
|
+
for (let j = lines.length - 1; j >= 0; j -= 1) {
|
|
503
|
+
const line = lines[j];
|
|
504
|
+
const cost = line.length + 1;
|
|
505
|
+
if (chars + cost > limit) {
|
|
506
|
+
if (chars === 0 && limit > 16) {
|
|
507
|
+
const truncated = `${line.slice(0, limit - 1)}…`;
|
|
508
|
+
picked.push(truncated);
|
|
509
|
+
chars = truncated.length;
|
|
510
|
+
}
|
|
511
|
+
return { lines: picked.reverse(), chars };
|
|
512
|
+
}
|
|
513
|
+
picked.push(line);
|
|
514
|
+
chars += cost;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return { lines: picked.reverse(), chars };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Compose the text a handoff carries into the fresh session, in this order:
|
|
522
|
+
* (i) the goal objective, (ii) the recorded handoff context / reasons (most
|
|
523
|
+
* recent first), (iii) the bounded tail of the earlier conversation (oldest
|
|
524
|
+
* first), after one line saying this is a trimmed continuation and one line
|
|
525
|
+
* naming the old session id.
|
|
526
|
+
*
|
|
527
|
+
* @param {{
|
|
528
|
+
* sessionId?: string|null,
|
|
529
|
+
* objective?: string|null,
|
|
530
|
+
* goalLabel?: string|null,
|
|
531
|
+
* reasons?: string[]|null,
|
|
532
|
+
* messages?: unknown,
|
|
533
|
+
* maxChars?: number,
|
|
534
|
+
* }} [options]
|
|
535
|
+
* @returns {{ text: string, carriedChars: number, tailChars: number, tailLines: number }}
|
|
536
|
+
*/
|
|
537
|
+
export function composeHandoff({
|
|
538
|
+
sessionId = null,
|
|
539
|
+
objective = null,
|
|
540
|
+
goalLabel = null,
|
|
541
|
+
reasons = null,
|
|
542
|
+
messages = [],
|
|
543
|
+
maxChars = HANDOFF_DEFAULT_MAX_CHARS,
|
|
544
|
+
} = {}) {
|
|
545
|
+
const budget = isFiniteNumber(maxChars) && maxChars > 0 ? Math.trunc(maxChars) : HANDOFF_DEFAULT_MAX_CHARS;
|
|
546
|
+
const oldId = sessionId == null || sessionId === '' ? '(unknown)' : String(sessionId);
|
|
547
|
+
const tail = handoffTail(messages, { budget });
|
|
548
|
+
const lines = [
|
|
549
|
+
'<continuum_handoff>',
|
|
550
|
+
'This is a trimmed continuation of an earlier session whose earlier content could not be carried because it exceeded the model context window.',
|
|
551
|
+
`Old session id: ${oldId}`,
|
|
552
|
+
];
|
|
553
|
+
|
|
554
|
+
// (i) the goal objective
|
|
555
|
+
const goalText = flattenText(objective);
|
|
556
|
+
if (goalText !== '') lines.push(`Goal objective: ${goalText}`);
|
|
557
|
+
else if (flattenText(goalLabel) !== '') lines.push(`Goal objective: (not recorded) — recorded goal ${flattenText(goalLabel)}`);
|
|
558
|
+
else lines.push('Goal objective: (no goal recorded)');
|
|
559
|
+
|
|
560
|
+
// (ii) the recorded handoff context of that session, most recent first
|
|
561
|
+
const recorded = Array.isArray(reasons)
|
|
562
|
+
? reasons.filter((reason) => typeof reason === 'string' && flattenText(reason) !== '').map(flattenText)
|
|
563
|
+
: [];
|
|
564
|
+
if (recorded.length > 0) {
|
|
565
|
+
lines.push('Recorded checkpoint context of that session (most recent first):');
|
|
566
|
+
for (const reason of recorded.slice().reverse()) lines.push(`- ${reason}`);
|
|
567
|
+
} else {
|
|
568
|
+
lines.push('Recorded checkpoint context of that session: (none recorded)');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// (iii) the bounded tail of the conversation, oldest first
|
|
572
|
+
if (tail.lines.length > 0) {
|
|
573
|
+
lines.push(`Trimmed tail of the earlier conversation (${tail.lines.length} line(s), ${tail.chars} character(s), oldest first):`);
|
|
574
|
+
for (const line of tail.lines) lines.push(line);
|
|
575
|
+
} else {
|
|
576
|
+
lines.push('Trimmed tail of the earlier conversation: (not available)');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
lines.push('Continue the work from here: this handoff is the only context carried over from the earlier session.');
|
|
580
|
+
lines.push('</continuum_handoff>');
|
|
581
|
+
const text = lines.join('\n');
|
|
582
|
+
return { text, carriedChars: text.length, tailChars: tail.chars, tailLines: tail.lines.length };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** A phrase that identifies a failure caused by an oversized context. */
|
|
586
|
+
const CONTEXT_WINDOW_PATTERN = /context[ _-]?(?:window|length)|context_length_exceeded|maximum context/i;
|
|
587
|
+
|
|
588
|
+
/** A phrase that identifies an exceeded limit next to {@link CONTEXT_WINDOW_PATTERN}. */
|
|
589
|
+
const EXCEED_PATTERN = /exceed|overflow|too (?:long|large|many)|over the limit|maximum/i;
|
|
590
|
+
|
|
591
|
+
/** The joined text of one message (used for overflow evidence). */
|
|
592
|
+
function messageTextOf(message) {
|
|
593
|
+
if (!isObject(message)) return '';
|
|
594
|
+
const parts = [];
|
|
595
|
+
if (typeof message.content === 'string') parts.push(message.content);
|
|
596
|
+
else if (Array.isArray(message.content)) {
|
|
597
|
+
for (const block of message.content) {
|
|
598
|
+
if (typeof block === 'string') parts.push(block);
|
|
599
|
+
else if (isObject(block) && typeof block.text === 'string') parts.push(block.text);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (typeof message.text === 'string') parts.push(message.text);
|
|
603
|
+
return parts.join(' ');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** The failure text of one event-ish object (shallow, never throws). */
|
|
607
|
+
function eventTextOf(event) {
|
|
608
|
+
if (!isObject(event)) return '';
|
|
609
|
+
const parts = [];
|
|
610
|
+
for (const key of ['message', 'error', 'reason', 'text']) {
|
|
611
|
+
const value = event[key];
|
|
612
|
+
if (typeof value === 'string') parts.push(value);
|
|
613
|
+
else if (isObject(value) && typeof value.message === 'string') parts.push(value.message);
|
|
614
|
+
}
|
|
615
|
+
if (isObject(event.data)) {
|
|
616
|
+
for (const key of ['message', 'error', 'text']) {
|
|
617
|
+
const value = event.data[key];
|
|
618
|
+
if (typeof value === 'string') parts.push(value);
|
|
619
|
+
else if (isObject(value) && typeof value.message === 'string') parts.push(value.message);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return parts.join(' ');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Decide whether a session is over the model context window.
|
|
627
|
+
*
|
|
628
|
+
* Evidence, in this order:
|
|
629
|
+
* 1. the newest assistant message's `usage.inputTokens` versus the session's
|
|
630
|
+
* `contextWindow`: overflow when the used tokens EXCEED the window;
|
|
631
|
+
* 2. a recent message / event whose text says the context window was exceeded.
|
|
632
|
+
*
|
|
633
|
+
* When the numbers are unknown, the verdict is NOT overflow: an unknown window
|
|
634
|
+
* must never block the normal resume path.
|
|
635
|
+
*
|
|
636
|
+
* @param {{ messages?: unknown, contextWindow?: number|null, events?: unknown }} [options]
|
|
637
|
+
* @returns {{ over: boolean, inputTokens: number|null, contextWindow: number|null, detail: string|null }}
|
|
638
|
+
*/
|
|
639
|
+
export function contextOverflow({ messages = [], contextWindow = null, events = [] } = {}) {
|
|
640
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
641
|
+
let inputTokens = null;
|
|
642
|
+
for (let i = list.length - 1; i >= 0; i -= 1) {
|
|
643
|
+
const message = list[i];
|
|
644
|
+
if (!isObject(message)) continue;
|
|
645
|
+
const usage = message.usage;
|
|
646
|
+
if (!isObject(usage)) continue;
|
|
647
|
+
for (const key of ['inputTokens', 'promptTokens', 'input_tokens']) {
|
|
648
|
+
if (isFiniteNumber(usage[key]) && usage[key] >= 0) {
|
|
649
|
+
inputTokens = usage[key];
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (inputTokens !== null) break;
|
|
654
|
+
}
|
|
655
|
+
const window = isFiniteNumber(contextWindow) && contextWindow > 0 ? contextWindow : null;
|
|
656
|
+
|
|
657
|
+
let failure = '';
|
|
658
|
+
const scan = (text) => {
|
|
659
|
+
if (failure !== '' || typeof text !== 'string' || text === '') return;
|
|
660
|
+
if (CONTEXT_WINDOW_PATTERN.test(text) && EXCEED_PATTERN.test(text)) failure = flattenText(text).slice(0, 200);
|
|
661
|
+
};
|
|
662
|
+
for (let i = list.length - 1; i >= 0 && failure === ''; i -= 1) scan(messageTextOf(list[i]));
|
|
663
|
+
const eventList = Array.isArray(events) ? events.slice(-20) : [];
|
|
664
|
+
for (let i = eventList.length - 1; i >= 0 && failure === ''; i -= 1) scan(eventTextOf(eventList[i]));
|
|
665
|
+
|
|
666
|
+
const numbers = inputTokens !== null && window !== null
|
|
667
|
+
? ` (used ${inputTokens} of ${window} tokens)`
|
|
668
|
+
: inputTokens !== null
|
|
669
|
+
? ` (used ${inputTokens} tokens; window unknown)`
|
|
670
|
+
: '';
|
|
671
|
+
|
|
672
|
+
if (failure !== '') {
|
|
673
|
+
return { over: true, inputTokens, contextWindow: window, detail: `a recent turn failed with a context-window error${numbers}: ${failure}` };
|
|
674
|
+
}
|
|
675
|
+
if (inputTokens !== null && window !== null && inputTokens > window) {
|
|
676
|
+
return {
|
|
677
|
+
over: true,
|
|
678
|
+
inputTokens,
|
|
679
|
+
contextWindow: window,
|
|
680
|
+
detail: `the last request used ${inputTokens} tokens, which exceeds the model context window of ${window}`,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
over: false,
|
|
685
|
+
inputTokens,
|
|
686
|
+
contextWindow: window,
|
|
687
|
+
detail: inputTokens !== null && window !== null ? `used ${inputTokens} of ${window} tokens` : null,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
304
691
|
/**
|
|
305
692
|
* One-line human-readable summary for logs and the health endpoint.
|
|
306
693
|
*
|
package/lib/restart.js
CHANGED
|
File without changes
|
package/locale/en.json
CHANGED
|
File without changes
|
package/locale/zh.json
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-autorestart",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "DeepSeek Harness plugin: restart without losing work. Waits until every conversation is idle, restarts the host, then automatically resumes the interrupted sessions. AI-generated, not human-reviewed: use with caution.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh",
|
|
@@ -34,7 +34,10 @@
|
|
|
34
34
|
"cordis.patch.yml",
|
|
35
35
|
"locale/*.json",
|
|
36
36
|
"icon.svg",
|
|
37
|
-
"
|
|
37
|
+
"assets/*.png",
|
|
38
|
+
"screenshots.json",
|
|
39
|
+
"README.md",
|
|
40
|
+
"README.zh.md"
|
|
38
41
|
],
|
|
39
42
|
"dsh": {
|
|
40
43
|
"bundle": {
|