mixdog 0.9.12 → 0.9.14
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/package.json +1 -1
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +11 -7
- package/src/defaults/cycle3-review-prompt.md +14 -0
- package/src/defaults/memory-promote-prompt.md +9 -9
- package/src/lib/rules-builder.cjs +15 -11
- package/src/mixdog-session-runtime.mjs +10 -0
- package/src/rules/agent/00-common.md +5 -17
- package/src/rules/agent/00-core.md +21 -0
- package/src/rules/lead/lead-brief.md +7 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +39 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +0 -25
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +120 -3
- package/src/runtime/agent/orchestrator/agent-trace.mjs +43 -1
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +48 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +42 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +24 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +61 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +57 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +40 -4
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +0 -62
- package/src/runtime/agent/orchestrator/session/compact.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +12 -9
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +48 -157
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +6 -23
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +0 -13
- package/src/runtime/agent/orchestrator/session/loop.mjs +32 -77
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +33 -45
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +132 -11
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -6
- package/src/runtime/agent/orchestrator/session/manager.mjs +30 -15
- package/src/runtime/agent/orchestrator/stall-policy.mjs +16 -0
- package/src/runtime/channels/index.mjs +28 -1
- package/src/runtime/channels/lib/memory-client.mjs +200 -15
- package/src/runtime/memory/index.mjs +18 -13
- package/src/runtime/memory/lib/core-memory-store.mjs +108 -4
- package/src/runtime/memory/lib/cycle-scheduler.mjs +52 -18
- package/src/runtime/memory/lib/memory-cycle1.mjs +166 -23
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +37 -25
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +37 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +17 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +106 -6
- package/src/runtime/memory/lib/memory-embed.mjs +35 -1
- package/src/runtime/memory/lib/memory.mjs +7 -1
- package/src/runtime/memory/lib/query-handlers.mjs +1 -0
- package/src/standalone/agent-tool.mjs +89 -7
- package/src/tui/dist/index.mjs +203 -10
- package/src/tui/engine/tui-steering-persist.mjs +175 -0
- package/src/tui/engine.mjs +46 -2
- package/src/ui/statusline.mjs +2 -4
- package/src/workflows/default/WORKFLOW.md +2 -0
- package/src/workflows/sequential/WORKFLOW.md +2 -0
- package/src/workflows/solo/WORKFLOW.md +2 -0
|
@@ -24,7 +24,7 @@ import { join } from 'path'
|
|
|
24
24
|
import { fileURLToPath } from 'url'
|
|
25
25
|
import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
|
|
26
26
|
import { callAgentDispatch } from './agent-ipc.mjs'
|
|
27
|
-
import { listCore, editCore, deleteCore, CORE_SUMMARY_MAX } from './core-memory-store.mjs'
|
|
27
|
+
import { listCore, editCore, deleteCore, archiveCore, CORE_SUMMARY_MAX } from './core-memory-store.mjs'
|
|
28
28
|
import { loadCurrentRulesDigest } from './memory-cycle2.mjs'
|
|
29
29
|
import { embedText } from './embedding-provider.mjs'
|
|
30
30
|
import { searchRelevantHybrid } from './memory-recall-store.mjs'
|
|
@@ -207,6 +207,16 @@ function parseVerdicts(raw, idSet) {
|
|
|
207
207
|
actions.push({ id, verb: 'merge', targetId, sourceIds })
|
|
208
208
|
} else if (verb === 'delete') {
|
|
209
209
|
actions.push({ id, verb: 'delete' })
|
|
210
|
+
} else if (verb === 'superseded' || verb === 'supersede') {
|
|
211
|
+
// Require newer-id proof: `id|superseded|<newer_id>`. Without a valid
|
|
212
|
+
// newer active core id the supersession has no evidence → drop to keep.
|
|
213
|
+
const newerId = Number((parts[2] ?? '').trim())
|
|
214
|
+
if (!Number.isFinite(newerId) || !idSet.has(newerId) || newerId === id) {
|
|
215
|
+
__mixdogMemoryLog(`[cycle3] superseded rejected: id=${id} invalid/missing newer_id=${parts[2] ?? ''} → keep\n`)
|
|
216
|
+
actions.push({ id, verb: 'keep' })
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
actions.push({ id, verb: 'superseded', newerId })
|
|
210
220
|
}
|
|
211
221
|
}
|
|
212
222
|
if (!sawValid) return null
|
|
@@ -481,9 +491,66 @@ async function _runCycle3Impl(db, config, dataDir, options = {}) {
|
|
|
481
491
|
if (!actionIds.has(id)) parsed.actions.push({ id, verb: 'keep' })
|
|
482
492
|
}
|
|
483
493
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
494
|
+
// Supersession target liveness: a `superseded` verdict points at a newerId
|
|
495
|
+
// that must remain LIVE after this batch. If newerId is itself superseded/
|
|
496
|
+
// delete/merged-away in the same batch, archiving against it retires a row
|
|
497
|
+
// against a non-live replacement. Resolve the newerId transitively through
|
|
498
|
+
// chained supersessions to the final live target; if that target is retired
|
|
499
|
+
// (deleted/merged-away or a supersession cycle/dead-end) → downgrade to keep.
|
|
500
|
+
{
|
|
501
|
+
const byId = new Map(parsed.actions.map(a => [Number(a.id), a]))
|
|
502
|
+
// Two-phase, cycle-safe. Phase 1 computes every resolution against a FROZEN
|
|
503
|
+
// snapshot of the original verbs/newerIds — the walk never observes another
|
|
504
|
+
// action's in-progress mutation, so an A→B, B→A mutual cycle can't have A
|
|
505
|
+
// downgrade first and then let B see A as live. Any id that enters a
|
|
506
|
+
// supersession cycle yields null (no live target) for ALL its members.
|
|
507
|
+
// Phase 2 applies the computed resolutions in one pass.
|
|
508
|
+
const snap = new Map(parsed.actions.map(a => [Number(a.id), {
|
|
509
|
+
verb: a.verb,
|
|
510
|
+
newerId: a.newerId != null ? Number(a.newerId) : null,
|
|
511
|
+
id: Number(a.id),
|
|
512
|
+
sourceIds: a.sourceIds,
|
|
513
|
+
targetId: a.targetId,
|
|
514
|
+
}]))
|
|
515
|
+
// ids removed from the live pool by their own verdict (not superseded —
|
|
516
|
+
// that is chased transitively below).
|
|
517
|
+
const retiredBySnap = (s) => s && (s.verb === 'delete' ||
|
|
518
|
+
(s.verb === 'merge' && s.sourceIds?.includes(s.id) && s.targetId !== s.id))
|
|
519
|
+
const resolveLiveTarget = (startId) => {
|
|
520
|
+
let cur = Number(startId)
|
|
521
|
+
const seen = new Set()
|
|
522
|
+
while (true) {
|
|
523
|
+
if (seen.has(cur)) return null // supersession cycle → no live target
|
|
524
|
+
seen.add(cur)
|
|
525
|
+
const s = snap.get(cur)
|
|
526
|
+
if (!s) return null
|
|
527
|
+
if (retiredBySnap(s)) return null // target itself removed this batch
|
|
528
|
+
if (s.verb === 'superseded') { cur = Number(s.newerId); continue }
|
|
529
|
+
return cur // keep/update/merge-survivor → live
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Phase 1: resolve against frozen snapshot.
|
|
533
|
+
const resolutions = []
|
|
534
|
+
for (const a of parsed.actions) {
|
|
535
|
+
if (a.verb !== 'superseded') continue
|
|
536
|
+
resolutions.push({ action: a, live: resolveLiveTarget(a.newerId) })
|
|
537
|
+
}
|
|
538
|
+
// Phase 2: apply.
|
|
539
|
+
for (const { action: a, live } of resolutions) {
|
|
540
|
+
if (live == null) {
|
|
541
|
+
__mixdogMemoryLog(`[cycle3] superseded downgraded to keep: id=${a.id} newerId=${a.newerId} not live after batch\n`)
|
|
542
|
+
a.verb = 'keep'
|
|
543
|
+
delete a.newerId
|
|
544
|
+
} else if (live !== Number(a.newerId)) {
|
|
545
|
+
__mixdogMemoryLog(`[cycle3] superseded newerId resolved transitively: id=${a.id} ${a.newerId}->${live}\n`)
|
|
546
|
+
a.newerId = live
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let kept = 0, updated = 0, merged = 0, deleted = 0, superseded = 0
|
|
552
|
+
const proposed = { kept: 0, updated: 0, merged: 0, deleted: 0, superseded: 0 }
|
|
553
|
+
const held = { updated: 0, merged: 0, deleted: 0, superseded: 0 }
|
|
487
554
|
const details = []
|
|
488
555
|
const touched = new Set() // ids already acted on this cycle — avoid double action
|
|
489
556
|
|
|
@@ -550,6 +617,38 @@ async function _runCycle3Impl(db, config, dataDir, options = {}) {
|
|
|
550
617
|
}
|
|
551
618
|
continue
|
|
552
619
|
}
|
|
620
|
+
if (a.verb === 'superseded') {
|
|
621
|
+
proposed.superseded++
|
|
622
|
+
// Supersession archives (status flip), never physical DELETE, and applies
|
|
623
|
+
// in DEFAULT (conservative) mode — reversible and audit-retained, so it is
|
|
624
|
+
// safe without the confirmed gate that outright deletes require.
|
|
625
|
+
if (!mutate) {
|
|
626
|
+
held.superseded++
|
|
627
|
+
details.push({
|
|
628
|
+
id: a.id, verb: 'superseded', applied: false, held: true,
|
|
629
|
+
reason: 'proposal mode',
|
|
630
|
+
})
|
|
631
|
+
touched.add(a.id)
|
|
632
|
+
continue
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
const core = coreById.get(a.id)
|
|
636
|
+
const res = await archiveCore(dataDir, a.id, core ? { element: core.element, summary: core.summary } : null)
|
|
637
|
+
if (res?.skipped) {
|
|
638
|
+
held.superseded++
|
|
639
|
+
details.push({ id: a.id, verb: 'superseded', newerId: a.newerId, applied: false, held: true, reason: res.reason })
|
|
640
|
+
} else {
|
|
641
|
+
superseded++
|
|
642
|
+
details.push({ id: a.id, verb: 'superseded', newerId: a.newerId, applied: true })
|
|
643
|
+
}
|
|
644
|
+
touched.add(a.id)
|
|
645
|
+
} catch (err) {
|
|
646
|
+
if (signal?.aborted) throw signal.reason ?? err
|
|
647
|
+
__mixdogMemoryLog(`[cycle3] supersede archive failed id=${a.id}: ${err.message}\n`)
|
|
648
|
+
details.push({ id: a.id, verb: 'superseded', error: err.message })
|
|
649
|
+
}
|
|
650
|
+
continue
|
|
651
|
+
}
|
|
553
652
|
if (a.verb === 'merge') {
|
|
554
653
|
if (a.targetId !== a.id && !a.sourceIds.includes(a.id)) {
|
|
555
654
|
__mixdogMemoryLog(
|
|
@@ -638,9 +737,10 @@ async function _runCycle3Impl(db, config, dataDir, options = {}) {
|
|
|
638
737
|
`[cycle3] reviewed=${cores.length} kept=${kept}` +
|
|
639
738
|
` proposed_update=${proposed.updated} proposed_merge=${proposed.merged} proposed_delete=${proposed.deleted}` +
|
|
640
739
|
` applied_update=${updated} applied_merge=${merged} applied_delete=${deleted}` +
|
|
641
|
-
`
|
|
740
|
+
` applied_superseded=${superseded}` +
|
|
741
|
+
` held_update=${held.updated} held_merge=${held.merged} held_delete=${held.deleted} held_superseded=${held.superseded}` +
|
|
642
742
|
` mode=${applyMode}\n`,
|
|
643
743
|
)
|
|
644
744
|
|
|
645
|
-
return { reviewed: cores.length, kept, updated, merged, deleted, proposed, held, applied: mutate, applyMode, details }
|
|
745
|
+
return { reviewed: cores.length, kept, updated, merged, deleted, superseded, proposed, held, applied: mutate, applyMode, details }
|
|
646
746
|
}
|
|
@@ -430,7 +430,41 @@ async function syncBatchEmbeddings(db, ids, options = {}) {
|
|
|
430
430
|
throwIfAborted(signal)
|
|
431
431
|
const writtenIds = (res.rows || []).map(r => Number(r.id))
|
|
432
432
|
if (writtenIds.length < updates.length) {
|
|
433
|
-
|
|
433
|
+
const writtenSet = new Set(writtenIds)
|
|
434
|
+
const missed = updates.filter((u) => !writtenSet.has(u.id))
|
|
435
|
+
const missedIds = missed.map((u) => u.id)
|
|
436
|
+
const curRows = missedIds.length
|
|
437
|
+
? (await db.query(
|
|
438
|
+
`SELECT id, embedding IS NOT NULL AS has_embedding, element, summary
|
|
439
|
+
FROM memory.entries WHERE id = ANY($1::bigint[]) AND is_root = 1`,
|
|
440
|
+
[missedIds],
|
|
441
|
+
)).rows
|
|
442
|
+
: []
|
|
443
|
+
const curById = new Map(curRows.map((r) => [Number(r.id), r]))
|
|
444
|
+
let staleSkips = 0
|
|
445
|
+
let concurrentSkips = 0
|
|
446
|
+
for (const u of missed) {
|
|
447
|
+
const cur = curById.get(u.id)
|
|
448
|
+
if (!cur) continue
|
|
449
|
+
const textUnchanged =
|
|
450
|
+
(cur.element === u.element || (cur.element == null && u.element == null)) &&
|
|
451
|
+
(cur.summary === u.summary || (cur.summary == null && u.summary == null))
|
|
452
|
+
if (!textUnchanged) {
|
|
453
|
+
staleSkips++
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
if (cur.has_embedding) {
|
|
457
|
+
concurrentSkips++
|
|
458
|
+
continue
|
|
459
|
+
}
|
|
460
|
+
staleSkips++
|
|
461
|
+
}
|
|
462
|
+
if (staleSkips > 0) {
|
|
463
|
+
__mixdogMemoryLog(`[embed-sync] ${staleSkips} stale-write(s) skipped — text changed since read\n`)
|
|
464
|
+
}
|
|
465
|
+
if (concurrentSkips > 0 && process.env.MIXDOG_DEBUG_EMBED) {
|
|
466
|
+
__mixdogMemoryLog(`[embed-sync] ${concurrentSkips} concurrent embed write(s) skipped — already embedded\n`)
|
|
467
|
+
}
|
|
434
468
|
}
|
|
435
469
|
return writtenIds
|
|
436
470
|
}
|
|
@@ -455,6 +455,11 @@ export async function ensureCurrentSchemaExtensions(db, dims) {
|
|
|
455
455
|
// re-nominates the same entry.
|
|
456
456
|
await db.exec(`ALTER TABLE entries ADD COLUMN IF NOT EXISTS core_candidate_status text`)
|
|
457
457
|
await db.exec(`ALTER TABLE entries ADD COLUMN IF NOT EXISTS core_candidate_at bigint`)
|
|
458
|
+
// cycle3 supersession retirement: archive (status flip) instead of physical
|
|
459
|
+
// DELETE for facts a newer active fact replaced. Additive nullable columns —
|
|
460
|
+
// legacy rows read as NULL status = active. No data migration required.
|
|
461
|
+
await db.exec(`ALTER TABLE core_entries ADD COLUMN IF NOT EXISTS status text`)
|
|
462
|
+
await db.exec(`ALTER TABLE core_entries ADD COLUMN IF NOT EXISTS archived_at bigint`)
|
|
458
463
|
// No index on core_candidate_status by design (round-2 finding #4): the only
|
|
459
464
|
// readers are listCoreCandidates (user picker, on-demand) and
|
|
460
465
|
// nominateCoreCandidates (once per cycle2, hourly). Both are rare and the
|
|
@@ -470,7 +475,8 @@ export async function ensureCurrentSchemaExtensions(db, dims) {
|
|
|
470
475
|
SELECT id,
|
|
471
476
|
row_number() OVER (
|
|
472
477
|
PARTITION BY project_id, element
|
|
473
|
-
ORDER BY
|
|
478
|
+
ORDER BY (CASE WHEN status IS NULL OR status = 'active' THEN 0 ELSE 1 END),
|
|
479
|
+
updated_at DESC NULLS LAST, id DESC
|
|
474
480
|
) AS rn
|
|
475
481
|
FROM core_entries
|
|
476
482
|
), deleted AS (
|
|
@@ -241,6 +241,7 @@ export function createQueryHandlers({
|
|
|
241
241
|
(${hitExpr}) AS hit_count
|
|
242
242
|
FROM core_entries
|
|
243
243
|
WHERE ${where.join(' AND ')}
|
|
244
|
+
AND (status IS NULL OR status = 'active')
|
|
244
245
|
ORDER BY hit_count DESC, updated_at DESC, id ASC
|
|
245
246
|
LIMIT $${params.length}
|
|
246
247
|
`, params)).rows
|
|
@@ -16,9 +16,13 @@ import { updateJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
|
|
|
16
16
|
import { normalizeAgentPermission } from '../runtime/shared/markdown-frontmatter.mjs';
|
|
17
17
|
import { prepareAgentSession } from '../runtime/agent/orchestrator/agent-runtime/session-builder.mjs';
|
|
18
18
|
import {
|
|
19
|
+
abortAgentProgressWatchdog,
|
|
19
20
|
agentWatchdogPolicyActive,
|
|
20
21
|
evaluateAgentWatchdogAbort,
|
|
21
22
|
resolveAgentWatchdogPolicy,
|
|
23
|
+
resolveHandoffMessageStartIndex,
|
|
24
|
+
watchdogPartialHandoffFromError,
|
|
25
|
+
AgentStallAbortError,
|
|
22
26
|
} from '../runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs';
|
|
23
27
|
import { buildAgentTaskProgressFields } from './agent-task-status.mjs';
|
|
24
28
|
import { AGENT_OWNER } from '../runtime/agent/orchestrator/agent-owner.mjs';
|
|
@@ -962,13 +966,15 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
962
966
|
}, notifyContext);
|
|
963
967
|
}
|
|
964
968
|
|
|
965
|
-
function startProgressIdleWatchdog(sessionId, watchdogPolicy) {
|
|
969
|
+
function startProgressIdleWatchdog(sessionId, watchdogPolicy, agent = null) {
|
|
966
970
|
if (!sessionId || !agentWatchdogPolicyActive(watchdogPolicy)) return null;
|
|
967
971
|
if (typeof mgr.getSessionProgressSnapshot !== 'function' && typeof mgr.getSessionLastProgressAt !== 'function') return null;
|
|
968
972
|
if (typeof mgr.linkParentSignalToSession !== 'function') return null;
|
|
969
973
|
const controller = new AbortController();
|
|
974
|
+
const anchorTs = Date.now();
|
|
970
975
|
try { mgr.linkParentSignalToSession(sessionId, controller.signal); } catch { return null; }
|
|
971
976
|
const timer = setInterval(() => {
|
|
977
|
+
if (controller.signal?.aborted) return;
|
|
972
978
|
const now = Date.now();
|
|
973
979
|
const snapshot = typeof mgr.getSessionProgressSnapshot === 'function'
|
|
974
980
|
? mgr.getSessionProgressSnapshot(sessionId)
|
|
@@ -976,15 +982,36 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
976
982
|
const abortErr = snapshot
|
|
977
983
|
? evaluateAgentWatchdogAbort(snapshot, now, watchdogPolicy)
|
|
978
984
|
: null;
|
|
985
|
+
const sess = typeof mgr.getSession === 'function' ? mgr.getSession(sessionId) : null;
|
|
986
|
+
const iteration = typeof sess?.lastIterationIndex === 'number' ? sess.lastIterationIndex : null;
|
|
979
987
|
if (!abortErr && !snapshot) {
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
988
|
+
const reported = mgr.getSessionLastProgressAt(sessionId);
|
|
989
|
+
const last = reported || anchorTs;
|
|
990
|
+
if (watchdogPolicy.idleStaleMs > 0 && now - last > watchdogPolicy.idleStaleMs) {
|
|
991
|
+
abortAgentProgressWatchdog(controller, {
|
|
992
|
+
sessionId,
|
|
993
|
+
agent,
|
|
994
|
+
error: new AgentStallAbortError(`agent task stale (${watchdogPolicy.idleStaleMs}ms without progress)`),
|
|
995
|
+
policy: watchdogPolicy,
|
|
996
|
+
now,
|
|
997
|
+
anchorTs,
|
|
998
|
+
lastProgressAt: reported,
|
|
999
|
+
iteration,
|
|
1000
|
+
});
|
|
983
1001
|
}
|
|
984
1002
|
return;
|
|
985
1003
|
}
|
|
986
1004
|
if (abortErr) {
|
|
987
|
-
|
|
1005
|
+
abortAgentProgressWatchdog(controller, {
|
|
1006
|
+
sessionId,
|
|
1007
|
+
agent,
|
|
1008
|
+
error: abortErr,
|
|
1009
|
+
snapshot,
|
|
1010
|
+
policy: watchdogPolicy,
|
|
1011
|
+
now,
|
|
1012
|
+
anchorTs,
|
|
1013
|
+
iteration,
|
|
1014
|
+
});
|
|
988
1015
|
}
|
|
989
1016
|
}, 1000);
|
|
990
1017
|
timer.unref?.();
|
|
@@ -1072,7 +1099,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1072
1099
|
|
|
1073
1100
|
async function runSpawn(prepared, notifyContext = null, job = null) {
|
|
1074
1101
|
const { args, tag, session, agent, preset, presetName, workerCwd, prompt, watchdogPolicy } = prepared;
|
|
1075
|
-
const watchdog = startProgressIdleWatchdog(session.id, watchdogPolicy);
|
|
1102
|
+
const watchdog = startProgressIdleWatchdog(session.id, watchdogPolicy, agent);
|
|
1076
1103
|
let finalStatus = 'idle';
|
|
1077
1104
|
// SubagentStart: a worker session is about to run its first turn.
|
|
1078
1105
|
emitSubagentEvent('start', agent, { session_id: session.id, tag });
|
|
@@ -1086,6 +1113,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1086
1113
|
status: 'running',
|
|
1087
1114
|
stage: 'running',
|
|
1088
1115
|
});
|
|
1116
|
+
let handoffMsgStart = 0;
|
|
1089
1117
|
try {
|
|
1090
1118
|
const completionValue = (result) => {
|
|
1091
1119
|
// Promote an abnormal finish (iteration cap, truncation, or a public
|
|
@@ -1107,6 +1135,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1107
1135
|
...(abnormalError ? { error: abnormalError } : {}),
|
|
1108
1136
|
};
|
|
1109
1137
|
};
|
|
1138
|
+
handoffMsgStart = resolveHandoffMessageStartIndex(mgr.getSession(session.id));
|
|
1110
1139
|
const result = await mgr.askSession(session.id, prompt, args.context || null, null, workerCwd, null, {
|
|
1111
1140
|
notifyFn: workerNotifyFn(session.id, notifyContext || {}),
|
|
1112
1141
|
...(job ? {
|
|
@@ -1155,6 +1184,33 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1155
1184
|
}
|
|
1156
1185
|
return finalValue;
|
|
1157
1186
|
} catch (error) {
|
|
1187
|
+
const partial = watchdogPartialHandoffFromError(error, mgr.getSession(session.id), handoffMsgStart);
|
|
1188
|
+
if (partial) {
|
|
1189
|
+
finalStatus = 'idle';
|
|
1190
|
+
const value = {
|
|
1191
|
+
tag,
|
|
1192
|
+
sessionId: session.id,
|
|
1193
|
+
agent,
|
|
1194
|
+
preset: presetKey(preset) || presetName,
|
|
1195
|
+
provider: preset.provider,
|
|
1196
|
+
model: preset.model,
|
|
1197
|
+
effort: preset.effort || null,
|
|
1198
|
+
fast: preset.fast === true,
|
|
1199
|
+
content: partial,
|
|
1200
|
+
stallAbort: true,
|
|
1201
|
+
};
|
|
1202
|
+
if (job) job._terminalResultValue = value;
|
|
1203
|
+
if (job?.taskId) {
|
|
1204
|
+
try {
|
|
1205
|
+
reconcileBackgroundTask(job.taskId, {
|
|
1206
|
+
status: 'completed',
|
|
1207
|
+
result: value,
|
|
1208
|
+
terminalReason: 'agent-watchdog-partial',
|
|
1209
|
+
});
|
|
1210
|
+
} catch {}
|
|
1211
|
+
}
|
|
1212
|
+
return value;
|
|
1213
|
+
}
|
|
1158
1214
|
finalStatus = 'error';
|
|
1159
1215
|
// Part C: a mid-stream stall (StreamStalledError / ESTREAMSTALL) throws
|
|
1160
1216
|
// here WITHOUT a terminal result, so the finally reconcile below (gated on
|
|
@@ -1229,10 +1285,11 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1229
1285
|
async function runSend(prepared, notifyContext = null, job = null) {
|
|
1230
1286
|
const { args, session, sessionId, prompt } = prepared;
|
|
1231
1287
|
const sendAgent = session.agent || normalizeAgentName(args.agent);
|
|
1232
|
-
const watchdog = startProgressIdleWatchdog(sessionId, resolveAgentWatchdogPolicy(sendAgent));
|
|
1288
|
+
const watchdog = startProgressIdleWatchdog(sessionId, resolveAgentWatchdogPolicy(sendAgent), sendAgent);
|
|
1233
1289
|
const tag = tagForSession(sessionId);
|
|
1234
1290
|
let finalStatus = 'idle';
|
|
1235
1291
|
upsertWorkerSessionDeferred(session, tag, { status: 'running', stage: 'running' });
|
|
1292
|
+
let handoffMsgStart = 0;
|
|
1236
1293
|
try {
|
|
1237
1294
|
const completionValue = (result) => {
|
|
1238
1295
|
// Same abnormal-empty → error promotion as runSpawn: a reused/`send`
|
|
@@ -1249,6 +1306,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1249
1306
|
...(abnormalError ? { error: abnormalError } : {}),
|
|
1250
1307
|
};
|
|
1251
1308
|
};
|
|
1309
|
+
handoffMsgStart = resolveHandoffMessageStartIndex(mgr.getSession(sessionId));
|
|
1252
1310
|
const result = await mgr.askSession(sessionId, prompt, args.context || null, null, session.cwd || defaultCwd, null, {
|
|
1253
1311
|
notifyFn: workerNotifyFn(sessionId, notifyContext || {}),
|
|
1254
1312
|
...(job ? {
|
|
@@ -1287,6 +1345,30 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1287
1345
|
}
|
|
1288
1346
|
return finalValue;
|
|
1289
1347
|
} catch (error) {
|
|
1348
|
+
const partial = watchdogPartialHandoffFromError(error, mgr.getSession(sessionId), handoffMsgStart);
|
|
1349
|
+
if (partial) {
|
|
1350
|
+
finalStatus = 'idle';
|
|
1351
|
+
const value = {
|
|
1352
|
+
tag,
|
|
1353
|
+
sessionId,
|
|
1354
|
+
agent: session.agent || null,
|
|
1355
|
+
provider: session.provider,
|
|
1356
|
+
model: session.model,
|
|
1357
|
+
content: partial,
|
|
1358
|
+
stallAbort: true,
|
|
1359
|
+
};
|
|
1360
|
+
if (job) job._terminalResultValue = value;
|
|
1361
|
+
if (job?.taskId) {
|
|
1362
|
+
try {
|
|
1363
|
+
reconcileBackgroundTask(job.taskId, {
|
|
1364
|
+
status: 'completed',
|
|
1365
|
+
result: value,
|
|
1366
|
+
terminalReason: 'agent-watchdog-partial',
|
|
1367
|
+
});
|
|
1368
|
+
} catch {}
|
|
1369
|
+
}
|
|
1370
|
+
return value;
|
|
1371
|
+
}
|
|
1290
1372
|
finalStatus = 'error';
|
|
1291
1373
|
// Part C (send path mirror): a mid-stream stall throws with no terminal
|
|
1292
1374
|
// result — reconcile to `failed` so the owner is notified rather than the
|