mixdog 1.3.2 → 1.3.3
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 +7 -7
- package/scripts/verify-runtime-dependency-versions.mjs +158 -0
- package/src/agents/reviewer/AGENT.md +5 -7
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +4 -0
- package/src/defaults/skills/browser-use/SKILL.md +6 -3
- package/src/defaults/skills/local-provider/SKILL.md +33 -6
- package/src/rules/lead/LEAD.md +5 -8
- package/src/rules/shared/45-destructive.md +5 -1
- package/src/rules/shared/60-verification.md +6 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +38 -11
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +17 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +13 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-runtime.mjs +5 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +10 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +40 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +32 -11
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +13 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +68 -12
- package/src/runtime/agent/orchestrator/session/compact/continuation.mjs +15 -6
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +31 -8
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +44 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +117 -19
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +147 -137
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +37 -74
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +13 -7
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/empty-nudge.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/output-limit.mjs +62 -13
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +22 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +12 -20
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/call-prep.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/manager/agent-listing-notice.mjs +102 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +24 -15
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +32 -1
- package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -9
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +7 -5
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/runtime-user-context.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/stale-sweep.mjs +5 -4
- package/src/runtime/agent/orchestrator/session/store/sweep/sub-agent-retention.mjs +140 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-plan.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +76 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +33 -1
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.d.mts +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +37 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +18 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +127 -20
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +557 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +8 -2
- package/src/runtime/attachments/legacy-office.mjs +110 -0
- package/src/runtime/attachments/prompt-restore.mjs +70 -0
- package/src/runtime/attachments/store.mjs +60 -3
- package/src/runtime/effort-judge/judge-client.mjs +61 -14
- package/src/runtime/effort-judge/model-install.mjs +34 -2
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +14 -2
- package/src/runtime/office/portable/portable-soffice.mjs +132 -30
- package/src/runtime/shared/agent-avatar-sprites.mjs +23 -8
- package/src/runtime/shared/agent-avatar.mjs +3 -2
- package/src/runtime/shared/agent-room-host.d.mts +31 -0
- package/src/runtime/shared/agent-room-host.mjs +58 -0
- package/src/runtime/shared/bot-hops.mjs +16 -11
- package/src/runtime/shared/redact-directories.mjs +18 -0
- package/src/runtime/shared/remove-directory.mjs +28 -0
- package/src/runtime/shared/session-sidebar-host.d.mts +11 -2
- package/src/runtime/shared/session-sidebar-host.mjs +16 -3
- package/src/runtime/shared/tool-execution-contract.mjs +4 -0
- package/src/runtime/shared/tool-work-units.mjs +12 -9
- package/src/runtime/shared/turn-snapshot.mjs +233 -122
- package/src/runtime/shared/user-data-guard.mjs +51 -19
- package/src/runtime/shared/worktrees/activity-gate.mjs +209 -227
- package/src/runtime/shared/worktrees/contract.mjs +11 -3
- package/src/runtime/shared/worktrees/maintenance.mjs +11 -2
- package/src/runtime/shared/worktrees/manager.mjs +419 -52
- package/src/runtime/shared/worktrees/registry.mjs +3 -1
- package/src/runtime/shared/worktrees/tool-host.mjs +39 -0
- package/src/runtime/tidy/process.mjs +41 -8
- package/src/runtime/tidy/resolve.mjs +10 -7
- package/src/session-runtime/agent-home-delivery.mjs +73 -91
- package/src/session-runtime/agent-profile-changes.mjs +16 -0
- package/src/session-runtime/agent-room-delivery.mjs +2 -63
- package/src/session-runtime/agent-task-sessions.mjs +194 -64
- package/src/session-runtime/boot/apis.mjs +3 -0
- package/src/session-runtime/boot/facade.mjs +5 -6
- package/src/session-runtime/boot/tools.mjs +21 -1
- package/src/session-runtime/held-requests.mjs +20 -25
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +5 -22
- package/src/session-runtime/model-recency.mjs +1 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +8 -1
- package/src/session-runtime/quick-model-rows/model-meta.mjs +2 -0
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +7 -1
- package/src/session-runtime/runtime-review-api.mjs +4 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +34 -19
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +3 -0
- package/src/session-runtime/services/provider-admin.mjs +12 -2
- package/src/session-runtime/services/worktree-access.mjs +27 -0
- package/src/session-runtime/session-hooks.mjs +9 -17
- package/src/session-runtime/settings-builtin-tools-api.mjs +2 -0
- package/src/session-runtime/setup-tool/executor.mjs +4 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -0
- package/src/session-runtime/tool-policy/agent-profile-fold.mjs +98 -14
- package/src/session-runtime/tool-policy/policy-stage.mjs +23 -5
- package/src/session-runtime/tool-policy-refresh.mjs +1 -0
- package/src/session-runtime/tool-surface.mjs +9 -5
- package/src/session-runtime/turn/session-ops.mjs +8 -0
- package/src/session-runtime/turn/turn-run.mjs +5 -1
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +10 -2
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +39 -17
- package/src/session-runtime/workspace/transition-steps.mjs +1 -18
- package/src/session-runtime/workspace/transitions.mjs +279 -67
- package/src/session-runtime/workspace/worktree-tool.mjs +194 -0
- package/src/standalone/daemon-lag-profiler.mjs +3 -1
- package/src/standalone/daemon-session-control.mjs +140 -23
- package/src/standalone/daemon-telemetry.mjs +9 -0
- package/src/standalone/daemon.mjs +5 -0
- package/src/standalone/local-session-runtime.mjs +11 -2
- package/src/standalone/session-protocol.mjs +1 -0
- package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +2 -0
- package/src/standalone/session-service/agent-route-follow.mjs +69 -0
- package/src/standalone/session-service/session-calls/session-turns.mjs +33 -4
- package/src/standalone/session-service.mjs +10 -0
- package/src/standalone/session-tool-output.mjs +1 -0
- package/src/standalone/staged-import.mjs +47 -0
- package/src/tui/App.jsx +2 -0
- package/src/tui/app/message-selector.mjs +15 -2
- package/src/tui/app/model-options.mjs +16 -2
- package/src/tui/app/model-picker.mjs +2 -1
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/dist/index.mjs +92 -40
- package/src/tui/paste-attachments.mjs +0 -1
- package/src/tui/paste-text-policy.mjs +4 -5
- package/src/tui/session/agent-job-feed/execution-delivery.mjs +4 -1
- package/src/tui/session/notice-surface.mjs +3 -0
- package/src/tui/session/session-api/intake/abort.mjs +33 -17
- package/src/tui/session/session-api/intake/rewind.mjs +39 -4
- package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +11 -48
- package/src/session-runtime/room-member-gate.mjs +0 -61
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
// Periodic idle-session + tombstone sweep.
|
|
3
3
|
// Drives sweepStaleSessions on an unref'd interval; closeSession is imported
|
|
4
4
|
// from session-close.mjs (one-way dependency, no cycle).
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
applySubAgentRetention,
|
|
7
|
+
sweepStaleSessions,
|
|
8
|
+
sweepStaleSessionsCooperative,
|
|
9
|
+
evictIdleLiveSessions,
|
|
10
|
+
} from '../store.mjs';
|
|
6
11
|
import { sweepOrphanedPendingMessages } from './pending-messages.mjs';
|
|
12
|
+
import { applySessionSidebarChange, readSessionSidebarState } from '../../../../shared/session-sidebar-host.mjs';
|
|
7
13
|
import {
|
|
8
14
|
IN_FLIGHT_STAGES,
|
|
9
15
|
_getRuntimeEntry,
|
|
@@ -83,6 +89,29 @@ function _logTombstoneSweep(tombstonesCleaned, tombstoneDetails, tombstoneErrors
|
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
// Sub-agent trees the sweep found idle: archived after 3 days (through the
|
|
93
|
+
// desktop's archive flag, when one is attached), deleted after 30
|
|
94
|
+
// (store/sweep/sub-agent-retention.mjs).
|
|
95
|
+
async function _retainSubAgents(members) {
|
|
96
|
+
if (!members?.length) return;
|
|
97
|
+
try {
|
|
98
|
+
const flags = await readSessionSidebarState();
|
|
99
|
+
const applied = await applySubAgentRetention(members, {
|
|
100
|
+
isSessionLive: _isSessionLive,
|
|
101
|
+
isArchived: flags && ((id) => Object.hasOwn(flags.archived, id)),
|
|
102
|
+
setSidebar: applySessionSidebarChange,
|
|
103
|
+
});
|
|
104
|
+
for (const id of applied.deleted) if (!_isSessionLive(id)) _clearSessionRuntime(id);
|
|
105
|
+
if (applied.archived.length || applied.deleted.length) {
|
|
106
|
+
_sweepLog(
|
|
107
|
+
`[session-sweep] sub-agent retention: archived ${applied.archived.length}${_previewIds(applied.archived.map((id) => ({ id })))}, deleted ${applied.deleted.length}${_previewIds(applied.deleted.map((id) => ({ id })))}\n`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
} catch (e) {
|
|
111
|
+
process.stderr.write(`[session-sweep] sub-agent retention error: ${e?.message || e}\n`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
async function sweepIdleSessions({ includeTombstones = true, sweepIdle = true } = {}) {
|
|
87
116
|
const startedAt = Date.now();
|
|
88
117
|
try {
|
|
@@ -90,8 +119,10 @@ async function sweepIdleSessions({ includeTombstones = true, sweepIdle = true }
|
|
|
90
119
|
sweepIdle,
|
|
91
120
|
tombstoneMaxAgeMs: includeTombstones ? TOMBSTONE_MAX_AGE_MS : 0,
|
|
92
121
|
isSessionLive: _isSessionLive,
|
|
122
|
+
retainSubAgents: sweepIdle,
|
|
93
123
|
});
|
|
94
124
|
const { cleaned, remaining, details, tombstonesCleaned = 0, tombstoneDetails = [], tombstoneErrors = [] } = result;
|
|
125
|
+
await _retainSubAgents(result.subAgents);
|
|
95
126
|
if (cleaned > 0) {
|
|
96
127
|
for (const d of details) {
|
|
97
128
|
if (!_finalizeSweptSessionRuntime(d)) continue;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// the provider accepted it. `from` is the mode the model was last told
|
|
16
16
|
// about, `to` the session's mode now.
|
|
17
17
|
// Every notice says it replaces the earlier ones, so the latest one wins.
|
|
18
|
-
import { normalizeOrchestrationMode } from '../../../../shared/orchestration.mjs';
|
|
18
|
+
import { normalizeOrchestrationMode, ORCHESTRATION_MODES } from '../../../../shared/orchestration.mjs';
|
|
19
19
|
import { orchestrationInstructions } from '../../runtime-core/orchestration.mjs';
|
|
20
20
|
|
|
21
21
|
/** The mode's name as its BP3 block titles it (`# Orchestration Mode: …`). */
|
|
@@ -31,6 +31,22 @@ export function promptOrchestrationModeOf(session) {
|
|
|
31
31
|
return session?.orchestrationMode;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Read a shipped coding-mode slice from the actual BP3 message, not the mode
|
|
36
|
+
* metadata: older profile refreshes could rewrite one without the other.
|
|
37
|
+
* Only a complete, unambiguous generated slice is evidence. Custom rules,
|
|
38
|
+
* headings quoted in another slice, and unrecognized legacy layouts are not.
|
|
39
|
+
*/
|
|
40
|
+
export function orchestrationModeFromPrompt(session) {
|
|
41
|
+
const core = session?.messages?.find(
|
|
42
|
+
(message) => message?.role === 'system' && message.cacheTier === 'tier3'
|
|
43
|
+
)?.content;
|
|
44
|
+
if (typeof core !== 'string') return null;
|
|
45
|
+
const slices = core.split('\n\n---\n\n');
|
|
46
|
+
const modes = ORCHESTRATION_MODES.filter((mode) => slices.includes(orchestrationInstructions(mode)));
|
|
47
|
+
return modes.length === 1 ? modes[0] : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
const listed = (names) => names.map((name) => `\`${name}\``).join(', ');
|
|
35
51
|
|
|
36
52
|
/** The reminder that makes the Lead follow `mode`: self-contained (the mode's
|
|
@@ -3,6 +3,7 @@ import { mergeNormalizedContentEntries } from '../loop/steering.mjs';
|
|
|
3
3
|
import { isDeliveredCompletion, logDuplicateSkip } from './delivered-completions.mjs';
|
|
4
4
|
import { promptContentText } from './prompt-utils.mjs';
|
|
5
5
|
import { parseTaskNotification, taskNotificationId } from '../../../../shared/task-notification-envelope.mjs';
|
|
6
|
+
import { hopOf } from '../../../../shared/bot-hops.mjs';
|
|
6
7
|
|
|
7
8
|
const STALE_USER_INJECTION_TTL_MS = 30 * 60 * 1000;
|
|
8
9
|
const PENDING_PROCESS_START_MS = Date.now();
|
|
@@ -95,11 +96,13 @@ function executionFromCompletionMeta(meta) {
|
|
|
95
96
|
function completionEntryFields(entry) {
|
|
96
97
|
const executionId = completionExecutionId(entry);
|
|
97
98
|
const execution = normalizeExecution(entry?.execution, executionId);
|
|
99
|
+
const hop = hopOf(entry?.hop);
|
|
98
100
|
return {
|
|
99
101
|
notificationKind: COMPLETION_NOTIFICATION_KIND,
|
|
100
102
|
mode: PENDING_MODE_TASK_NOTIFICATION,
|
|
101
103
|
...(executionId ? { executionId } : {}),
|
|
102
104
|
...(execution ? { execution } : {}),
|
|
105
|
+
...(hop ? { hop } : {}),
|
|
103
106
|
};
|
|
104
107
|
}
|
|
105
108
|
|
|
@@ -141,6 +144,8 @@ export function markCompletionEntry(text, options = {}) {
|
|
|
141
144
|
mode: PENDING_MODE_TASK_NOTIFICATION,
|
|
142
145
|
...(executionId ? { executionId } : {}),
|
|
143
146
|
...(execution ? { execution } : {}),
|
|
147
|
+
// The bot-chain hop of the turn that launched the work (bot-hops.mjs).
|
|
148
|
+
...(hopOf(options?.meta?.hop) ? { hop: options.meta.hop } : {}),
|
|
144
149
|
enqueuedAt: Date.now(),
|
|
145
150
|
};
|
|
146
151
|
}
|
|
@@ -337,6 +342,7 @@ export function _groupPendingMessageEntries(entries) {
|
|
|
337
342
|
...merged,
|
|
338
343
|
mode: PENDING_MODE_TASK_NOTIFICATION,
|
|
339
344
|
...(normalized.execution ? { execution: normalized.execution } : {}),
|
|
345
|
+
...(normalized.hop ? { hop: normalized.hop } : {}),
|
|
340
346
|
ids: idsOf([entry]),
|
|
341
347
|
entries: [entry],
|
|
342
348
|
});
|
|
@@ -329,6 +329,42 @@ export function withPresentedWorkspaceSlices(session, nextHead) {
|
|
|
329
329
|
return [nextHead, instructionFiles, memory].filter((part) => part?.trim()).join(BP3_PART_SEPARATOR);
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
const AGENT_CATALOG_HEADING = '# Available Agents';
|
|
333
|
+
// One generated catalog line (workflow/context.mjs agentCatalog):
|
|
334
|
+
// `- <name> (<id>)` with an optional `: <description>`.
|
|
335
|
+
const AGENT_CATALOG_ENTRY = /^- (.+?) \(([^()\s]+)\)(?:: .*)?$/;
|
|
336
|
+
|
|
337
|
+
/** The entries of a generated agent list slice, or null when `slice` is not
|
|
338
|
+
* one (only the exact generated format is evidence). */
|
|
339
|
+
export function agentListEntries(slice) {
|
|
340
|
+
const text = String(slice || '').trim();
|
|
341
|
+
if (!text.startsWith(`${AGENT_CATALOG_HEADING}\n\n`)) return null;
|
|
342
|
+
const entries = [];
|
|
343
|
+
for (const line of text.slice(AGENT_CATALOG_HEADING.length + 2).split('\n')) {
|
|
344
|
+
const match = AGENT_CATALOG_ENTRY.exec(line);
|
|
345
|
+
if (!match) return null;
|
|
346
|
+
entries.push({ id: match[2], name: match[1], line });
|
|
347
|
+
}
|
|
348
|
+
return entries;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The "# Available Agents" slice the session's prompt presents: '' when it
|
|
353
|
+
* carries none, null when what it carries under that heading is not one
|
|
354
|
+
* complete generated list (custom text, or more than one). A coding Lead's
|
|
355
|
+
* rebuilt core keeps a presented list, as it keeps the workspace slices: the
|
|
356
|
+
* list the model saw first stays the baseline every later agent list
|
|
357
|
+
* reminder applies to (agent-listing-notice.mjs).
|
|
358
|
+
*/
|
|
359
|
+
export function presentedAgentCatalogSlice(session) {
|
|
360
|
+
const core = session.bp3EnvSplit === true ? textOf(bp3SystemMessage(session)) : String(session.bp3CoreContext || '');
|
|
361
|
+
const slices = core
|
|
362
|
+
.split(BP3_PART_SEPARATOR)
|
|
363
|
+
.filter((slice) => slice === AGENT_CATALOG_HEADING || slice.startsWith(`${AGENT_CATALOG_HEADING}\n`));
|
|
364
|
+
if (!slices.length) return '';
|
|
365
|
+
return slices.length === 1 && agentListEntries(slices[0]) ? slices[0] : null;
|
|
366
|
+
}
|
|
367
|
+
|
|
332
368
|
const WORKSPACE_SLICES = ['environmentContext', 'coreMemoryContext', 'instructionFilesContext'];
|
|
333
369
|
|
|
334
370
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// manager/session-crud.mjs
|
|
2
2
|
// Session read + mutate CRUD: lookup (getSession/listSessions/findSessionByScopeKey),
|
|
3
3
|
// message clear (with clear-fork), manual compaction, status update, and metric flush.
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
import {
|
|
5
6
|
loadSession,
|
|
6
7
|
saveSessionAsync,
|
|
@@ -29,6 +30,7 @@ import { hideCompactionArchive } from '../compaction-archive-capture.mjs';
|
|
|
29
30
|
import { writeStderrBestEffort } from '../../../../shared/best-effort-stderr.mjs';
|
|
30
31
|
import { forgetSideQuestion } from '../side-question-store.mjs';
|
|
31
32
|
import { isAgentHomeSessionId } from '../agent-home.mjs';
|
|
33
|
+
import { messageIdOf } from '../message-ids.mjs';
|
|
32
34
|
|
|
33
35
|
/** Force-flush session metrics to disk. */
|
|
34
36
|
export async function flushSessionMetrics(sessionId) {
|
|
@@ -412,19 +414,36 @@ export async function updateSessionStatus(id, status) {
|
|
|
412
414
|
export async function rewindSessionMessagesTo(sessionId, options = {}) {
|
|
413
415
|
const session = loadSession(sessionId);
|
|
414
416
|
if (!session || session.closed === true) return null;
|
|
417
|
+
const targetId = String(options?.messageId ?? '').trim();
|
|
415
418
|
const target = String(options?.text ?? '').trim();
|
|
416
|
-
if (!target) return null;
|
|
419
|
+
if (!targetId && !target) return null;
|
|
417
420
|
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
418
|
-
|
|
421
|
+
// The stable message id is the identity: the stored prompt is the EXPANDED
|
|
422
|
+
// text (attachment tokens materialized), so display text cannot find it.
|
|
423
|
+
// Text is only the fallback for a row that carries no id.
|
|
424
|
+
const matches = targetId
|
|
425
|
+
? (message) => messageIdOf(message) === targetId
|
|
426
|
+
: (message) => promptContentText(message.content).trim() === target;
|
|
427
|
+
const found = [];
|
|
419
428
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
420
|
-
|
|
421
|
-
if (message?.role !== 'user') continue;
|
|
422
|
-
if (promptContentText(message.content).trim() !== target) continue;
|
|
423
|
-
start = index;
|
|
424
|
-
break;
|
|
429
|
+
if (messages[index]?.role === 'user' && matches(messages[index])) found.push(index);
|
|
425
430
|
}
|
|
426
|
-
|
|
431
|
+
// Without an id, text that several prompts repeat cannot say which one the
|
|
432
|
+
// user picked: refuse rather than rewind the wrong occurrence.
|
|
433
|
+
if (found.length === 0 || (!targetId && found.length > 1)) return null;
|
|
434
|
+
const start = found[0];
|
|
427
435
|
const removed = messages.length - start;
|
|
436
|
+
const rewoundMessage = messages[start];
|
|
437
|
+
// `dryRun` resolves the target and reports what a rewind would do, changing
|
|
438
|
+
// nothing: the caller prepares the restore payload first and commits after.
|
|
439
|
+
// The marker names the exact history the dry run saw (generation alone only
|
|
440
|
+
// moves on lifecycle changes); the commit passes it back and is refused when
|
|
441
|
+
// the history differs.
|
|
442
|
+
const historyMarker = `${session.generation ?? 0}:${messages.length}:${createHash('sha1')
|
|
443
|
+
.update(JSON.stringify(messages))
|
|
444
|
+
.digest('hex')}`;
|
|
445
|
+
if (options?.dryRun === true) return { removed, remaining: start, message: rewoundMessage, historyMarker };
|
|
446
|
+
if (options?.expectedHistoryMarker != null && options.expectedHistoryMarker !== historyMarker) return null;
|
|
428
447
|
session.messages = messages.slice(0, start);
|
|
429
448
|
// The provider cache is keyed on the exact prefix we just truncated;
|
|
430
449
|
// the prefix-guard snapshot and the /btw fork request describe it too and
|
|
@@ -435,5 +454,5 @@ export async function rewindSessionMessagesTo(sessionId, options = {}) {
|
|
|
435
454
|
session.updatedAt = Date.now();
|
|
436
455
|
session.lastUsedAt = Date.now();
|
|
437
456
|
await saveSessionAsync(session, { expectedGeneration: session.generation });
|
|
438
|
-
return { removed, remaining: session.messages.length };
|
|
457
|
+
return { removed, remaining: session.messages.length, message: rewoundMessage };
|
|
439
458
|
}
|
|
@@ -159,7 +159,7 @@ export function composeSessionSystem(opts, { profile, providerName, modelName, s
|
|
|
159
159
|
provider: providerName,
|
|
160
160
|
model: modelName,
|
|
161
161
|
});
|
|
162
|
-
const delegationFree = delegationDisabled(opts, ownerIsAgent);
|
|
162
|
+
const delegationFree = delegationDisabled({ ...opts, sessionMode }, ownerIsAgent);
|
|
163
163
|
const promptIdentity = {
|
|
164
164
|
systemPrompt: opts.systemPrompt || '',
|
|
165
165
|
skipAgentRules: opts.skipAgentRules === true,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// here is field derivation — no ownership, no prompt text; the only read is the
|
|
6
6
|
// parent's record a delegated session takes its root from.
|
|
7
7
|
import { toSessionWorkflowMeta } from '../../runtime-core/workflow.mjs';
|
|
8
|
-
import { delegationForNewSession } from '../human-gate/delegation.mjs';
|
|
8
|
+
import { delegationForNewSession, runForNewSession } from '../human-gate/delegation.mjs';
|
|
9
9
|
import { sessionOrchestrationMode } from '../../../../shared/orchestration.mjs';
|
|
10
10
|
import { preserveBufferConfigFields } from './context-meta.mjs';
|
|
11
11
|
import { positiveInt } from '../../../../shared/numbers.mjs';
|
|
@@ -103,16 +103,18 @@ export function sessionOriginFields(opts, { profile, presetObj, providerCacheOpt
|
|
|
103
103
|
ownerSessionId: opts.ownerSessionId || null,
|
|
104
104
|
visibility: opts.visibility || null,
|
|
105
105
|
clientHostPid: opts.clientHostPid || null,
|
|
106
|
-
// A sub-agent's
|
|
107
|
-
//
|
|
108
|
-
//
|
|
106
|
+
// A sub-agent's root and the run (if any) that started a sub-agent or a
|
|
107
|
+
// bot-created task session, derived from its creator — the one read here —
|
|
108
|
+
// never taken from the options (human-gate/delegation.mjs).
|
|
109
109
|
...delegationFields(opts),
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
function delegationFields(opts) {
|
|
114
114
|
const delegation = delegationForNewSession(opts);
|
|
115
|
-
|
|
115
|
+
// Work a schedule/webhook run started (a sub-agent, a task session) stays the run's.
|
|
116
|
+
const run = runForNewSession(opts);
|
|
117
|
+
return { ...(delegation ? { delegation } : {}), ...(run ? { run } : {}) };
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
// The persisted session record.
|
|
@@ -9,15 +9,19 @@ import { collectPromptSkillsCached } from '../../context/collect.mjs';
|
|
|
9
9
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
10
10
|
import { getHiddenAgent } from '../../internal-agents.mjs';
|
|
11
11
|
import { workflowDisallowsAgentTool } from '../../runtime-core/workflow.mjs';
|
|
12
|
+
import { isAgentModeSession } from '../agent-mode.mjs';
|
|
12
13
|
import { finalizeSessionToolList, resolveSessionTools, permissionFromToolSpec } from './tool-resolution.mjs';
|
|
13
14
|
import { unusedModelEditToolName } from '../../../../shared/edit-tool-dialect.mjs';
|
|
14
15
|
import { getAgentRuntimeSync } from './agent-runtime-singleton.mjs';
|
|
15
16
|
|
|
16
17
|
// A lead session may not delegate when its workflow forbids the agent tool
|
|
17
|
-
// (
|
|
18
|
-
// An agent-owned session never gets the tool anyway.
|
|
18
|
+
// (an agent-mode Lead without delegatable agents); Solo keeps it deferred for
|
|
19
|
+
// explicit user requests. An agent-owned session never gets the tool anyway.
|
|
19
20
|
export function delegationDisabled(source, ownerIsAgent) {
|
|
20
|
-
return
|
|
21
|
+
return (
|
|
22
|
+
!ownerIsAgent &&
|
|
23
|
+
workflowDisallowsAgentTool(source?.workflow, { agentMode: Boolean(source) && isAgentModeSession(source) })
|
|
24
|
+
);
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
// Exactly two schema surfaces exist: Lead and Agent. Every Agent role gets
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
markPendingGoalReminder,
|
|
41
41
|
snapshotPendingGoalReminder,
|
|
42
42
|
} from '../runtime-core/goal-reminder.mjs';
|
|
43
|
+
import { rearmLateDeferredTools } from '../runtime-core/tool-catalog.mjs';
|
|
43
44
|
|
|
44
45
|
async function setStage(opts, stage) {
|
|
45
46
|
try {
|
|
@@ -552,7 +553,15 @@ async function compactBeforeSend(ctx) {
|
|
|
552
553
|
return;
|
|
553
554
|
}
|
|
554
555
|
ctx.compactChanged = messagesArrayChanged(messages, run.compacted);
|
|
555
|
-
if (ctx.compactChanged)
|
|
556
|
+
if (ctx.compactChanged) {
|
|
557
|
+
adoptCompactedTranscript(ctx, run);
|
|
558
|
+
// The adopted compaction strips late MCP tool announcements from older turns.
|
|
559
|
+
try {
|
|
560
|
+
rearmLateDeferredTools(sessionRef);
|
|
561
|
+
} catch {
|
|
562
|
+
/* best-effort, like the Goal reminder: a tool reminder never fails the compaction */
|
|
563
|
+
}
|
|
564
|
+
}
|
|
556
565
|
reportCompactOutcome(ctx, run);
|
|
557
566
|
// PostCompact belongs only to a completed compaction pass, never the
|
|
558
567
|
// ordinary pre-send threshold check.
|
|
@@ -182,8 +182,10 @@ export function agentTaskRelayFailedMessage({ count, tries, at = Date.now(), mes
|
|
|
182
182
|
};
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
/**
|
|
186
|
-
* the agent to check with the
|
|
185
|
+
/** Bot-descended round trips (sent from a turn another bot's message started)
|
|
186
|
+
* on one task session before each report asks the agent to check with the
|
|
187
|
+
* user (session create). The user's or a routine's own delegations are not
|
|
188
|
+
* counted (agent-home-delivery.mjs `add`). */
|
|
187
189
|
export const TASK_LOOP_CAP = 3;
|
|
188
190
|
|
|
189
191
|
/** A coding task session's report into the home of the agent that created it
|
|
@@ -213,7 +215,8 @@ export function agentTaskMessage({
|
|
|
213
215
|
const summary = typeof givenSummary === 'string' ? givenSummary : routineSummary(text);
|
|
214
216
|
const reply = textChars > String(text ?? '').length ? { text: String(text), chars: textChars } : boundedReply(text);
|
|
215
217
|
// The files the turn produced: listed for the agent, stored ones carried as
|
|
216
|
-
// the row's attachments (
|
|
218
|
+
// the row's attachments (forwardable by id; the chat's result card does not
|
|
219
|
+
// show them, the task session does).
|
|
217
220
|
const produced = normalizeHandoffFiles(files);
|
|
218
221
|
const attachments = artifactRecords(produced);
|
|
219
222
|
const label = escapeHarnessTags(String(title).replace(/["\r\n]/g, ''));
|
|
@@ -310,7 +313,7 @@ export function agentTaskWakePrompt(messages, { more = 0 } = {}) {
|
|
|
310
313
|
'Relay the report above to the user now, briefly and in your own words; a report marked (working) is still in progress, not done.',
|
|
311
314
|
...(files
|
|
312
315
|
? [
|
|
313
|
-
'Its files are
|
|
316
|
+
'Its files are not shown in this chat: the user sees only the result card and can open the task session for details. Name the files that matter by path yourself, and pass them on to another session or agent with attachments.',
|
|
314
317
|
]
|
|
315
318
|
: []),
|
|
316
319
|
].join('\n\n');
|
|
@@ -33,7 +33,9 @@ export function stripRuntimeUserContext(message) {
|
|
|
33
33
|
} else {
|
|
34
34
|
return message;
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
// `agentListing` records what the stripped reminder told (agent-listing-
|
|
37
|
+
// notice.mjs): without the text it must not still count as told.
|
|
38
|
+
const { runtimeUserContext: _context, agentListing: _agentListing, ...meta } = message.meta;
|
|
37
39
|
const result = { ...message, content, meta };
|
|
38
40
|
if (Object.keys(meta).length === 0) delete result.meta;
|
|
39
41
|
return result;
|
|
@@ -256,7 +256,13 @@ export async function sendWithRecovery(ctx) {
|
|
|
256
256
|
let response;
|
|
257
257
|
try {
|
|
258
258
|
const { timedOpts } = hooks;
|
|
259
|
-
const
|
|
259
|
+
const baseOpts = timedOpts?.admissionOwner ? timedOpts : { ...(timedOpts || {}), admissionOwner: sessionId };
|
|
260
|
+
// A one-shot max_tokens request rides on this send's own copy only: the
|
|
261
|
+
// shared opts never carry it, so no later attempt can inherit it.
|
|
262
|
+
const admittedOpts =
|
|
263
|
+
ctx.maxOutputTokensOverride > 0
|
|
264
|
+
? { ...baseOpts, maxOutputTokensOverride: ctx.maxOutputTokensOverride }
|
|
265
|
+
: baseOpts;
|
|
260
266
|
// Ephemeral tail reminder (tail-reminder-rules.mjs): added to this request's
|
|
261
267
|
// copy only, never to the stored transcript.
|
|
262
268
|
const requestMessages = withTailReminder(messages, { session: ctx.sessionRef, provider, model });
|
|
@@ -267,7 +273,10 @@ export async function sendWithRecovery(ctx) {
|
|
|
267
273
|
stampStreamOutcome(sendErr, hooks.relayWitness);
|
|
268
274
|
return await recoverFromSendError(sendErr, { ...ctx, relayWitness: hooks.relayWitness });
|
|
269
275
|
}
|
|
270
|
-
|
|
276
|
+
// streamedChars: what this send put on screen through onTextDelta (net of
|
|
277
|
+
// acknowledged retractions), so the loop can withdraw it if it discards
|
|
278
|
+
// the response and re-sends.
|
|
279
|
+
return { action: 'proceed', response, streamedChars: hooks.relayWitness.text.length };
|
|
271
280
|
} finally {
|
|
272
281
|
hooks.restore();
|
|
273
282
|
}
|
|
@@ -21,8 +21,8 @@ function* sweepStaleSessionSteps(ttlMs, options = {}) {
|
|
|
21
21
|
// An unreadable store dir is not an empty one: sweeping (deleting,
|
|
22
22
|
// closing, pruning) on a probe we could not make is never allowed.
|
|
23
23
|
if (probePath(dir).state !== PROBE_PRESENT) {
|
|
24
|
-
const { openCandidates: _candidates, ...empty } = createSweepTally();
|
|
25
|
-
return empty;
|
|
24
|
+
const { openCandidates: _candidates, subAgents, ...empty } = createSweepTally();
|
|
25
|
+
return plan.retainSubAgents ? { ...empty, subAgents } : empty;
|
|
26
26
|
}
|
|
27
27
|
const rows = collectSweepRows(dir);
|
|
28
28
|
pruneSweepRecordCache(new Set(rows.map((row) => row?.id).filter(Boolean)));
|
|
@@ -41,8 +41,9 @@ function* sweepStaleSessionSteps(ttlMs, options = {}) {
|
|
|
41
41
|
yield* reapOrphanSidecars(ctx);
|
|
42
42
|
queueDeletedSummaryPrune(ctx.tally);
|
|
43
43
|
persistSweepRecordCache();
|
|
44
|
-
const { openCandidates: _candidates, ...result } = ctx.tally;
|
|
45
|
-
|
|
44
|
+
const { openCandidates: _candidates, subAgents, ...result } = ctx.tally;
|
|
45
|
+
// `subAgents` only for a caller that applies them (idle-cleanup.mjs).
|
|
46
|
+
return plan.retainSubAgents ? { ...result, subAgents } : result;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
/** Synchronous compatibility surface for explicit maintenance commands/tests. */
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// sweep/sub-agent-retention.mjs
|
|
2
|
+
// Retention of finished sub-agent sessions: the agent-only children (Worker,
|
|
3
|
+
// Reviewer, ...) a Lead's agents spawn, each its own stored session. The idle
|
|
4
|
+
// sweep names every sub-agent record with its STORED activity time (the clock
|
|
5
|
+
// survives restarts); a tree — a top sub-agent under a Lead with every
|
|
6
|
+
// sub-agent below it — is archived once each member is idle
|
|
7
|
+
// SUB_AGENT_ARCHIVE_AFTER_MS and deleted once each is idle
|
|
8
|
+
// SUB_AGENT_DELETE_AFTER_MS. Otherwise the whole tree stays as it is.
|
|
9
|
+
// Never a member: a root Lead, a task session from `session create`, a bot
|
|
10
|
+
// home chat or a group room's log/member session.
|
|
11
|
+
import { agentParentIds, isAgentOnlySession, isRootLeadSession } from '../../store-summary-visibility.mjs';
|
|
12
|
+
import { probePath, PROBE_PRESENT } from '../fs-probe.mjs';
|
|
13
|
+
import { sessionPath } from '../session-paths.mjs';
|
|
14
|
+
import { _queueSummaryIndexPrune } from '../summary-cache.mjs';
|
|
15
|
+
import { deleteSession } from '../session-delete.mjs';
|
|
16
|
+
import { readSweepRecord } from './sweep-record.mjs';
|
|
17
|
+
|
|
18
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
export const SUB_AGENT_ARCHIVE_AFTER_MS = 3 * DAY_MS;
|
|
20
|
+
export const SUB_AGENT_DELETE_AFTER_MS = 30 * DAY_MS;
|
|
21
|
+
const PERMANENT_ID = /^sess_(?:home|room|rmem)_/;
|
|
22
|
+
// The desktop change an automatic archive applies: that one session, no
|
|
23
|
+
// cascade (an explicit archive of a parent takes its sub-agents along).
|
|
24
|
+
export const SUB_AGENT_AUTO_ARCHIVE = Object.freeze({ archived: true, cascade: false });
|
|
25
|
+
|
|
26
|
+
const positive = (value) => (Number(value) > 0 ? Number(value) : 0);
|
|
27
|
+
|
|
28
|
+
/** Last activity of a stored record: its last conversation turn (`lastUsedAt`;
|
|
29
|
+
* `updatedAt` also moves on detach/resume bookkeeping) or its heartbeat. */
|
|
30
|
+
export function subAgentActivityAt(doc) {
|
|
31
|
+
return Math.max(
|
|
32
|
+
positive(doc?.lastUsedAt) || positive(doc?.updatedAt) || positive(doc?.createdAt),
|
|
33
|
+
positive(doc?.lastHeartbeatAt)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `{ id, parentIds, lastActive }` for a sub-agent record (open or closed);
|
|
38
|
+
* null for anything else. `parentIds` are the owner and parent links the
|
|
39
|
+
* record names (agentParentIds), the same ones an archive cascade follows. */
|
|
40
|
+
export function subAgentMember(id, doc) {
|
|
41
|
+
if (!doc || PERMANENT_ID.test(id) || doc.taskOrigin) return null;
|
|
42
|
+
if (isRootLeadSession(doc) || !isAgentOnlySession(doc)) return null;
|
|
43
|
+
return { id, parentIds: agentParentIds({ ...doc, id }), lastActive: subAgentActivityAt(doc) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The members grouped into trees: members joined by any owner/parent link
|
|
47
|
+
* between them (a link to a non-member joins nothing), each tree listing a
|
|
48
|
+
* top member (no member parent) first and its members below in link order. */
|
|
49
|
+
export function subAgentTrees(members) {
|
|
50
|
+
const byId = new Map(members.map((member) => [member.id, member]));
|
|
51
|
+
const links = new Map(members.map((member) => [member.id, new Set()]));
|
|
52
|
+
const hasMemberParent = new Set();
|
|
53
|
+
for (const member of members) {
|
|
54
|
+
for (const parent of (member.parentIds || []).filter((parentId) => byId.has(parentId))) {
|
|
55
|
+
links.get(member.id).add(parent);
|
|
56
|
+
links.get(parent).add(member.id);
|
|
57
|
+
hasMemberParent.add(member.id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
const trees = [];
|
|
62
|
+
for (const start of [...members.filter((m) => !hasMemberParent.has(m.id)), ...members]) {
|
|
63
|
+
if (seen.has(start.id)) continue;
|
|
64
|
+
const tree = [start];
|
|
65
|
+
seen.add(start.id);
|
|
66
|
+
for (let index = 0; index < tree.length; index++) {
|
|
67
|
+
for (const id of links.get(tree[index].id)) {
|
|
68
|
+
if (seen.has(id)) continue;
|
|
69
|
+
seen.add(id);
|
|
70
|
+
tree.push(byId.get(id));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
trees.push(tree);
|
|
74
|
+
}
|
|
75
|
+
return trees;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The stored activity time of `id` now; null when its record cannot be read. */
|
|
79
|
+
function storedActivityAt(id) {
|
|
80
|
+
const path = sessionPath(id);
|
|
81
|
+
const probe = probePath(path);
|
|
82
|
+
if (probe.state !== PROBE_PRESENT) return null;
|
|
83
|
+
const record = readSweepRecord(id, path, probe);
|
|
84
|
+
return record?.id === id ? subAgentActivityAt(record.doc) : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Archives or deletes each idle tree. A tree with a member running here
|
|
89
|
+
* (`isSessionLive`), or one whose stored activity moved since the sweep read
|
|
90
|
+
* it, is left alone. An archive goes through the desktop's archive flag
|
|
91
|
+
* (`setSidebar(id, SUB_AGENT_AUTO_ARCHIVE)`: exactly the members not yet
|
|
92
|
+
* archived, never a cascade past them — needs
|
|
93
|
+
* `isArchived`, the desktop's flags); a deleted member that was archived loses
|
|
94
|
+
* its flag (`{ forgotten: true }`). Returns `{ archived, deleted }` (ids).
|
|
95
|
+
*/
|
|
96
|
+
export async function applySubAgentRetention(
|
|
97
|
+
members,
|
|
98
|
+
{ isSessionLive = null, isArchived = null, setSidebar = null, now = Date.now() } = {}
|
|
99
|
+
) {
|
|
100
|
+
const archived = [];
|
|
101
|
+
const deleted = [];
|
|
102
|
+
for (const tree of subAgentTrees(members)) {
|
|
103
|
+
// A member of unknown age (no valid stored timestamp) keeps its tree.
|
|
104
|
+
if (!tree.every((member) => Number.isFinite(member.lastActive) && member.lastActive > 0)) continue;
|
|
105
|
+
const idle = now - Math.max(...tree.map((member) => member.lastActive));
|
|
106
|
+
const expired = idle >= SUB_AGENT_DELETE_AFTER_MS;
|
|
107
|
+
if (!expired && (idle < SUB_AGENT_ARCHIVE_AFTER_MS || !isArchived)) continue;
|
|
108
|
+
if (tree.some((member) => isSessionLive?.(member.id))) continue;
|
|
109
|
+
if (tree.some((member) => storedActivityAt(member.id) !== member.lastActive)) continue;
|
|
110
|
+
if (expired) {
|
|
111
|
+
for (const { id } of tree.toReversed()) {
|
|
112
|
+
try {
|
|
113
|
+
if (deleteSession(id, { deferSummaryUpdate: true, isSessionLive, legacyOwnerVeto: true })) deleted.push(id);
|
|
114
|
+
} catch {
|
|
115
|
+
/* kept; the next pass decides again */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
for (const { id } of tree) {
|
|
121
|
+
if (isArchived(id)) continue;
|
|
122
|
+
try {
|
|
123
|
+
await setSidebar(id, SUB_AGENT_AUTO_ARCHIVE);
|
|
124
|
+
archived.push(id);
|
|
125
|
+
} catch {
|
|
126
|
+
/* not archived; the next pass decides again */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (deleted.length) _queueSummaryIndexPrune(new Set(deleted));
|
|
131
|
+
for (const id of deleted) {
|
|
132
|
+
if (!isArchived?.(id)) continue;
|
|
133
|
+
try {
|
|
134
|
+
await setSidebar(id, { forgotten: true });
|
|
135
|
+
} catch {
|
|
136
|
+
/* a stale archive stamp hides nothing */
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { archived, deleted };
|
|
140
|
+
}
|
|
@@ -26,6 +26,9 @@ export function resolveSweepPlan(ttlMs, options = {}) {
|
|
|
26
26
|
retainOpen: sweepIdle && options.retainOpenSessions !== false,
|
|
27
27
|
openMaxAgeMs: Number.isFinite(optAge) && optAge > 0 ? optAge : RESUMABLE_OPEN_MAX_AGE_MS,
|
|
28
28
|
openMaxCount: Number.isFinite(optCount) && optCount >= 0 ? optCount : RESUMABLE_OPEN_MAX_COUNT,
|
|
29
|
+
// Name every sub-agent session for the tree retention its caller applies
|
|
30
|
+
// (sweep/sub-agent-retention.mjs). Idle sweep only.
|
|
31
|
+
retainSubAgents: sweepIdle && options.retainSubAgents === true,
|
|
29
32
|
};
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -69,5 +72,7 @@ export function createSweepTally() {
|
|
|
69
72
|
openCandidates: [],
|
|
70
73
|
openPruned: 0,
|
|
71
74
|
openPrunedDetails: [],
|
|
75
|
+
// Sub-agent sessions with their stored activity (plan.retainSubAgents).
|
|
76
|
+
subAgents: [],
|
|
72
77
|
};
|
|
73
78
|
}
|
|
@@ -48,7 +48,7 @@ export const BLANK_SCRATCH_MAX_AGE_MS = 60 * 60 * 1000; // 1h
|
|
|
48
48
|
// running is still parsed again.
|
|
49
49
|
const sweepRecordCache = new Map();
|
|
50
50
|
const SWEEP_RECORD_CACHE_FILE = 'session-sweep-records.json';
|
|
51
|
-
const SWEEP_RECORD_CACHE_VERSION =
|
|
51
|
+
const SWEEP_RECORD_CACHE_VERSION = 3;
|
|
52
52
|
let sweepRecordCacheDataDir = null;
|
|
53
53
|
let sweepRecordCacheDirty = false;
|
|
54
54
|
let sweepRecordParses = 0;
|
|
@@ -82,7 +82,7 @@ function syncSweepRecordCache() {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// The only document fields sweep-row reads (effectiveFields, the closed
|
|
85
|
-
// check). Caching the whole top-level document kept
|
|
85
|
+
// check, the sub-agent retention verdict). Caching the whole top-level document kept
|
|
86
86
|
// every session's tool catalogs, schemas and provider state alive — one
|
|
87
87
|
// entry per session file, measured at ~290MB of a 470MB daemon heap.
|
|
88
88
|
const SWEEP_DOC_FIELDS = [
|
|
@@ -97,6 +97,11 @@ const SWEEP_DOC_FIELDS = [
|
|
|
97
97
|
'createdAt',
|
|
98
98
|
'ownerSessionId',
|
|
99
99
|
'parentSessionId',
|
|
100
|
+
'lastUsedAt',
|
|
101
|
+
'agent',
|
|
102
|
+
'visibility',
|
|
103
|
+
'sessionVisibility',
|
|
104
|
+
'taskOrigin',
|
|
100
105
|
];
|
|
101
106
|
|
|
102
107
|
function conversationCountOf(doc) {
|