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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Lead's managed-worktree operations behind the `git` tool
|
|
3
|
+
* (`action: "worktree"` and the mapped `git worktree …` commands), over the
|
|
4
|
+
* same workspace authority the desktop branch menu drives:
|
|
5
|
+
*
|
|
6
|
+
* - The caller's own session runs through this runtime's workspace API with
|
|
7
|
+
* `ownTurn` (the calling turn is tolerated; workers, background shells and
|
|
8
|
+
* other sessions in an affected folder are not).
|
|
9
|
+
* - Another session runs through session control (the daemon's
|
|
10
|
+
* `session.configure` lane): no `ownTurn`, so a target with a running turn,
|
|
11
|
+
* workers or background shells refuses (WORKTREE_BUSY_JOBS).
|
|
12
|
+
*
|
|
13
|
+
* Every failure keeps its typed WorktreeError (code, details); interactive
|
|
14
|
+
* refusals (WORKTREE_DIRTY_CHOICE, WORKTREE_LARGE_REPO_CONFIRM) carry what the
|
|
15
|
+
* caller needs to ask the user before it calls again.
|
|
16
|
+
*/
|
|
17
|
+
import { worktreeError, worktreeNameError } from '../../runtime/shared/worktrees/contract.mjs';
|
|
18
|
+
import { samePath } from '../../runtime/shared/worktrees/paths.mjs';
|
|
19
|
+
import { assertWorktreeMutationAllowed, worktreeCallerScope } from '../services/worktree-access.mjs';
|
|
20
|
+
|
|
21
|
+
const loadManager = () => import('../../runtime/shared/worktrees/manager.mjs');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} deps
|
|
25
|
+
* @param {() => any} deps.getSession The Lead session this runtime hosts (its trusted caller scope).
|
|
26
|
+
* @param {() => any} deps.getWorkspaceApi The runtime's workspace transitions (late-bound).
|
|
27
|
+
* @param {((args: object, context: object) => Promise<any>)|null} [deps.executeSessionControl] Cross-session lane.
|
|
28
|
+
* @param {object|null} [deps.manager] Managed-worktree backend override (tests).
|
|
29
|
+
*/
|
|
30
|
+
export function createWorktreeToolHost({
|
|
31
|
+
getSession,
|
|
32
|
+
getWorkspaceApi,
|
|
33
|
+
executeSessionControl = null,
|
|
34
|
+
manager = null,
|
|
35
|
+
}) {
|
|
36
|
+
const backend = () => (manager ? Promise.resolve(manager) : loadManager());
|
|
37
|
+
|
|
38
|
+
function workspaceApi() {
|
|
39
|
+
const api = getWorkspaceApi();
|
|
40
|
+
if (!api) throw new Error('git worktree: managed worktrees are unavailable in this runtime');
|
|
41
|
+
return api;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The workspace lane of `sessionId`: the caller's own (null/equal) or
|
|
46
|
+
* another session's, which carries the caller's trusted `scope` for the
|
|
47
|
+
* daemon to check again.
|
|
48
|
+
*/
|
|
49
|
+
function laneFor(sessionId, callerSessionId, scope) {
|
|
50
|
+
if (!sessionId || sessionId === callerSessionId) {
|
|
51
|
+
const api = workspaceApi();
|
|
52
|
+
return {
|
|
53
|
+
own: true,
|
|
54
|
+
menu: () => api.getBranchMenu(),
|
|
55
|
+
call: (method, input) => api[method](input, { ownTurn: true }),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (typeof executeSessionControl !== 'function') {
|
|
59
|
+
throw new Error("git worktree: another session's workspace is unavailable in this runtime");
|
|
60
|
+
}
|
|
61
|
+
const control = (method, input) =>
|
|
62
|
+
executeSessionControl(
|
|
63
|
+
{ action: 'worktree', session_id: sessionId, method, ...(input ? { input } : {}) },
|
|
64
|
+
{ callerSessionId, callerScope: scope }
|
|
65
|
+
);
|
|
66
|
+
return { own: false, menu: () => control('getBranchMenu'), call: control };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function enter(lane, request) {
|
|
70
|
+
if (request.branch) {
|
|
71
|
+
return lane.call('selectBranch', {
|
|
72
|
+
branch: request.branch,
|
|
73
|
+
create: request.create === true,
|
|
74
|
+
confirmLarge: request.confirmLarge === true,
|
|
75
|
+
txId: request.txId,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// A valid slug names the worktree; any other text is a hint the slug is made from.
|
|
79
|
+
const name = typeof request.name === 'string' ? request.name.trim() : '';
|
|
80
|
+
const slug = name && !worktreeNameError(name) ? name : null;
|
|
81
|
+
return lane.call('requestWorktree', {
|
|
82
|
+
provision: true,
|
|
83
|
+
name: slug,
|
|
84
|
+
prompt: slug ? '' : name,
|
|
85
|
+
...(request.carry ? { carryChanges: request.carry } : {}),
|
|
86
|
+
confirmLarge: request.confirmLarge === true,
|
|
87
|
+
txId: request.txId,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Back to the project folder (the branch checked out there); the worktree stays retained. */
|
|
92
|
+
async function back(lane, request) {
|
|
93
|
+
const menu = await lane.menu();
|
|
94
|
+
const workspace = menu.workspace;
|
|
95
|
+
if (!workspace.worktreeId && samePath(workspace.cwd, workspace.projectPath)) {
|
|
96
|
+
return { revision: workspace.revision, workspace, unchanged: true };
|
|
97
|
+
}
|
|
98
|
+
const projectBranch = menu.occupancy.find((row) => row.kind === 'current')?.branch ?? null;
|
|
99
|
+
if (!projectBranch) {
|
|
100
|
+
throw worktreeError(
|
|
101
|
+
'WORKTREE_UNSUPPORTED_REPO',
|
|
102
|
+
'The project folder has no checked-out branch to return to (detached HEAD).'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return lane.call('selectBranch', { branch: projectBranch, expectedRevision: workspace.revision, txId: request.txId });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The desktop's "merge": into the confirmed merge target, then the cleanup autoCleanup allows. */
|
|
109
|
+
async function merge(lane) {
|
|
110
|
+
const { workspace } = await lane.menu();
|
|
111
|
+
return lane.call('mergeWorkspace', {
|
|
112
|
+
confirmed: true,
|
|
113
|
+
target: workspace.mergeTarget ?? null,
|
|
114
|
+
expectedRevision: workspace.revision,
|
|
115
|
+
cleanup: true,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* `git worktree remove <path>`: the managed removal of whichever owner
|
|
121
|
+
* holds it — this session's snapshot, or the worktree a session retains
|
|
122
|
+
* (its own lane: the same guards as the desktop's removal).
|
|
123
|
+
*/
|
|
124
|
+
async function removePath(request, callerSessionId, scope) {
|
|
125
|
+
const entry = await (await backend()).getWorktreeByPath(request.path);
|
|
126
|
+
if (!entry) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`git worktree remove: ${request.path} is not a Mixdog-managed worktree; unmanaged worktrees are not removed through the git tool`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (entry.kind === 'snapshot') return workspaceApi().removeSnapshot({ path: entry.path, force: request.force });
|
|
132
|
+
if (!entry.ownerSessionId) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`git worktree remove: no session retains ${entry.path}; git worktree prune removes it once it is clean and merged`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const lane = laneFor(entry.ownerSessionId, callerSessionId, scope);
|
|
138
|
+
const menu = await lane.menu();
|
|
139
|
+
if (menu.retainedWorktree?.id !== entry.id) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`git worktree remove: ${entry.path} is not the current worktree of session ${entry.ownerSessionId}; enter it (worktree_action enter, branch ${entry.branch}) first, then remove it`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return lane.call('removeWorktree', { force: request.force === true, expectedRevision: menu.workspace.revision });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {{ op: string } & Record<string, any>} request
|
|
149
|
+
* @param {{ callerSessionId: string }} context
|
|
150
|
+
*/
|
|
151
|
+
async function run(request, { callerSessionId }) {
|
|
152
|
+
// Trusted state of the calling Lead, never model input: every operation
|
|
153
|
+
// but `list` needs an attended conversation.
|
|
154
|
+
const scope = worktreeCallerScope(getSession());
|
|
155
|
+
if (request.op !== 'list') assertWorktreeMutationAllowed(scope, `git worktree ${request.op}`);
|
|
156
|
+
const lane = () => laneFor(request.sessionId, callerSessionId, scope);
|
|
157
|
+
switch (request.op) {
|
|
158
|
+
case 'enter':
|
|
159
|
+
return enter(lane(), request);
|
|
160
|
+
case 'return':
|
|
161
|
+
return back(lane(), request);
|
|
162
|
+
case 'merge':
|
|
163
|
+
return merge(lane());
|
|
164
|
+
case 'remove':
|
|
165
|
+
return lane().call('removeWorktree', { force: request.force === true });
|
|
166
|
+
case 'list':
|
|
167
|
+
return workspaceApi().listWorktrees({ includeSnapshots: true });
|
|
168
|
+
case 'prune':
|
|
169
|
+
return workspaceApi().pruneWorktrees({ repoCommonDir: request.repoCommonDir ?? null });
|
|
170
|
+
case 'add':
|
|
171
|
+
return workspaceApi().addBranchWorktree({
|
|
172
|
+
branch: request.branch,
|
|
173
|
+
create: request.create === true,
|
|
174
|
+
startPoint: request.startPoint ?? null,
|
|
175
|
+
confirmLarge: request.confirmLarge === true,
|
|
176
|
+
txId: request.txId,
|
|
177
|
+
repoCommonDir: request.repoCommonDir ?? null,
|
|
178
|
+
});
|
|
179
|
+
case 'snapshot':
|
|
180
|
+
return workspaceApi().createSnapshot({
|
|
181
|
+
commit: request.commit,
|
|
182
|
+
confirmLarge: request.confirmLarge === true,
|
|
183
|
+
txId: request.txId,
|
|
184
|
+
repoCommonDir: request.repoCommonDir ?? null,
|
|
185
|
+
});
|
|
186
|
+
case 'remove-path':
|
|
187
|
+
return removePath(request, callerSessionId, scope);
|
|
188
|
+
default:
|
|
189
|
+
throw new TypeError(`git worktree: unknown operation ${request.op}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { run };
|
|
194
|
+
}
|
|
@@ -20,7 +20,9 @@ export const LAG_PROFILE_KEEP = 10;
|
|
|
20
20
|
export const LAG_PROFILE_TOP = 15;
|
|
21
21
|
export const LAG_PROFILE_SAMPLING_US = 1000;
|
|
22
22
|
export const LAG_P99_THRESHOLD_MS = 1000;
|
|
23
|
-
|
|
23
|
+
// The owner lease's stall threshold: any window holding one logged
|
|
24
|
+
// `event loop stalled` line is kept.
|
|
25
|
+
export const LAG_MAX_THRESHOLD_MS = 1000;
|
|
24
26
|
|
|
25
27
|
const PROFILE_FILE_RE = /^daemon-lag-.+\.cpuprofile$/;
|
|
26
28
|
const SRC_ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
@@ -1,25 +1,85 @@
|
|
|
1
1
|
import { SESSION_ID_PATTERN } from '../runtime/agent/orchestrator/runtime-core/session-id.mjs';
|
|
2
2
|
import { sessionListOutput, sessionReadOutput, SESSION_TOOL_OUTPUT_BYTES } from './session-tool-output.mjs';
|
|
3
3
|
import { createAgentTaskSessions } from '../session-runtime/agent-task-sessions.mjs';
|
|
4
|
+
import { assertWorktreeMutationAllowed } from '../session-runtime/services/worktree-access.mjs';
|
|
4
5
|
import { createBotStatus, scanCatalog, scopedBot, sessionBotOf } from '../session-runtime/bot-status.mjs';
|
|
5
6
|
import { handoffContent } from '../session-runtime/handoff-attachments.mjs';
|
|
6
7
|
import { BOT_HOP_LIMIT_ERROR, MAX_BOT_HOPS } from '../runtime/shared/bot-hops.mjs';
|
|
7
8
|
import { rebindDelegation } from '../runtime/agent/orchestrator/session/human-gate/delegation.mjs';
|
|
9
|
+
import { isAgentOnlySession } from '../runtime/agent/orchestrator/session/store-summary-visibility.mjs';
|
|
10
|
+
import { readSessionSidebarState } from '../runtime/shared/session-sidebar-host.mjs';
|
|
11
|
+
import { agentRoomHost } from '../runtime/shared/agent-room-host.mjs';
|
|
12
|
+
import {
|
|
13
|
+
roomIdOfSession,
|
|
14
|
+
roomMemberOfSession,
|
|
15
|
+
roomSessionId,
|
|
16
|
+
} from '../runtime/agent/orchestrator/session/agent-room.mjs';
|
|
8
17
|
|
|
9
18
|
const TARGET_ACTIONS = new Set(['status', 'read', 'send', 'cancel', 'delete']);
|
|
10
19
|
const MUTATING_ACTIONS = new Set(['send', 'cancel', 'delete']);
|
|
20
|
+
// The workspace methods `worktree` reaches on another session (all on the
|
|
21
|
+
// session protocol's read/configure allowlists).
|
|
22
|
+
const WORKTREE_METHODS = new Set(['getBranchMenu', 'requestWorktree', 'selectBranch', 'mergeWorkspace', 'removeWorktree']);
|
|
11
23
|
// Why a send from a conversation that is no bot's reports nothing back.
|
|
12
24
|
const NOT_BOT_REASON = 'only a bot conversation gets session results reported back; check that session for the result';
|
|
13
25
|
|
|
26
|
+
/** The member a send to room log `sessionId` addresses, through the desktop
|
|
27
|
+
* room host: `{ host, agentId, sessionId, text }`; throws when no desktop is
|
|
28
|
+
* attached or no member resolves. */
|
|
29
|
+
async function roomAddressee(sessionId, args) {
|
|
30
|
+
let host;
|
|
31
|
+
try {
|
|
32
|
+
host = agentRoomHost();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new Error(`session: cannot send to ${sessionId}: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
const member = await host.member(sessionId, { to: args.to, prompt: args.prompt }).catch((error) => {
|
|
37
|
+
throw new Error(`session: ${error?.message || error}`);
|
|
38
|
+
});
|
|
39
|
+
return { host, ...member };
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
/** Model-facing session operations over the same service used by desktop and TUI. */
|
|
15
43
|
export function createSessionControl({
|
|
16
44
|
getSessionService,
|
|
17
45
|
agentTasks = createAgentTaskSessions({ getSessionService }),
|
|
18
46
|
botStatus = createBotStatus({ getSessionService }),
|
|
19
47
|
}) {
|
|
48
|
+
/**
|
|
49
|
+
* Another session's workspace for the git tool's worktree operations
|
|
50
|
+
* (session-runtime/workspace/worktree-tool.mjs): one workspace method with
|
|
51
|
+
* one input, through the same configure / read lanes the desktop uses. No
|
|
52
|
+
* `ownTurn` ever crosses: the target must be idle. A change (anything but
|
|
53
|
+
* the menu read) needs the caller's trusted scope (`callerScope`, from
|
|
54
|
+
* worktree-access.mjs worktreeCallerScope) to be an attended conversation.
|
|
55
|
+
*/
|
|
56
|
+
async function worktreeControl(args, caller, callerScope) {
|
|
57
|
+
const sessionId = typeof args.session_id === 'string' ? args.session_id.trim() : '';
|
|
58
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) throw new Error('session: session_id is required for action=worktree');
|
|
59
|
+
if (sessionId === caller) throw new Error('session: worktree control must target another session');
|
|
60
|
+
if (!WORKTREE_METHODS.has(args.method)) throw new Error(`session: unsupported worktree method "${args.method}"`);
|
|
61
|
+
if (args.method !== 'getBranchMenu') assertWorktreeMutationAllowed(callerScope, `session: worktree ${args.method}`);
|
|
62
|
+
const service = getSessionService();
|
|
63
|
+
if (!service) throw new Error('session: session service is unavailable');
|
|
64
|
+
if (args.method === 'getBranchMenu') {
|
|
65
|
+
return (await service.readSession({ sessionId, action: 'getBranchMenu', args: [] })).value;
|
|
66
|
+
}
|
|
67
|
+
const result = await service.configureSession(
|
|
68
|
+
{ sessionId, action: args.method, args: [args.input ?? {}] },
|
|
69
|
+
{ revision: 1 }
|
|
70
|
+
);
|
|
71
|
+
return result.value;
|
|
72
|
+
}
|
|
73
|
+
|
|
20
74
|
return async function executeSessionControl(args = {}, context = {}) {
|
|
21
75
|
const action = args.action;
|
|
22
|
-
if (
|
|
76
|
+
if (
|
|
77
|
+
action !== 'list' &&
|
|
78
|
+
action !== 'create' &&
|
|
79
|
+
action !== 'bots' &&
|
|
80
|
+
action !== 'worktree' &&
|
|
81
|
+
!TARGET_ACTIONS.has(action)
|
|
82
|
+
) {
|
|
23
83
|
throw new Error(`session: unsupported remote action "${action}"`);
|
|
24
84
|
}
|
|
25
85
|
const caller = context.callerSessionId;
|
|
@@ -28,6 +88,7 @@ export function createSessionControl({
|
|
|
28
88
|
}
|
|
29
89
|
// `callerAgentId`: set only for an attended agent conversation (workspace-tools.mjs).
|
|
30
90
|
if (action === 'create') return agentTasks.create(args, context);
|
|
91
|
+
if (action === 'worktree') return worktreeControl(args, caller, context.callerScope);
|
|
31
92
|
// `callerScope`: the caller's schedule-access scope (workspace-tools.mjs).
|
|
32
93
|
if (action === 'bots') return botStatus(args, context);
|
|
33
94
|
const sessionId = typeof args.session_id === 'string' ? args.session_id.trim() : '';
|
|
@@ -53,30 +114,70 @@ export function createSessionControl({
|
|
|
53
114
|
const service = getSessionService();
|
|
54
115
|
if (!service) throw new Error('session: session service is unavailable');
|
|
55
116
|
if (action === 'list') {
|
|
117
|
+
for (const flag of ['include_agents', 'include_archived']) {
|
|
118
|
+
if (args[flag] !== undefined && typeof args[flag] !== 'boolean') {
|
|
119
|
+
throw new Error(`session: ${flag} must be a boolean`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
56
122
|
// Scoped like bots (`callerScope`, bot-status.mjs): the human's sessions
|
|
57
123
|
// list everything, a bot (or its unattended run) only its own sessions,
|
|
58
|
-
// anything else the plain rows without previews.
|
|
124
|
+
// anything else the plain rows without previews. Sub-agent sessions (a
|
|
125
|
+
// parent finds its own through the agent tool) and archived ones (the
|
|
126
|
+
// desktop's flag) only on request; the scan pages past the rows left
|
|
127
|
+
// out, so a page is never short while more rows match.
|
|
59
128
|
const own = scopedBot(context.callerScope);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
129
|
+
const archived = (await readSessionSidebarState())?.archived ?? {};
|
|
130
|
+
const keep = (row) =>
|
|
131
|
+
(!own || sessionBotOf(row) === own) &&
|
|
132
|
+
(args.include_agents === true || !isAgentOnlySession(row)) &&
|
|
133
|
+
(args.include_archived === true || !Object.hasOwn(archived, row.id));
|
|
134
|
+
// Bounded: past 10000 rows scanned with fewer than `limit` matching, the
|
|
135
|
+
// page ends (possibly empty) with a nextCursor to continue from.
|
|
136
|
+
const scan = await scanCatalog(service, keep, { cursor: args.cursor || null, want: limit, maxRows: 10_000 });
|
|
137
|
+
const sessions = scan.rows.map((row) => (Object.hasOwn(archived, row.id) ? { ...row, archived: true } : row));
|
|
138
|
+
return sessionListOutput({ sessions, nextCursor: scan.nextCursor }, limit, { rich: own !== null });
|
|
69
139
|
}
|
|
70
140
|
if (action === 'send') {
|
|
71
141
|
if (typeof args.prompt !== 'string' || !args.prompt.trim()) {
|
|
72
142
|
throw new Error('session: prompt is required for action=send');
|
|
73
143
|
}
|
|
144
|
+
// A room member's session is the room's to drive: a message goes to the
|
|
145
|
+
// room, naming the member.
|
|
146
|
+
if (roomMemberOfSession(sessionId)) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`session: ${sessionId} is a group-room member's session; send to its room (session_id ${roomSessionId(roomMemberOfSession(sessionId).roomId)}) with to: <member>`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
// A send to a room's log is a message to one member there (`to`, or a
|
|
152
|
+
// leading @name), run by the desktop room host; the member's turn in
|
|
153
|
+
// its member session is the turn that answers and reports.
|
|
154
|
+
const room = roomIdOfSession(sessionId) ? await roomAddressee(sessionId, args) : null;
|
|
155
|
+
if (room?.sessionId === caller) throw new Error('session: send must target another session');
|
|
74
156
|
// An agent conversation's send reports its turn back to the agent's
|
|
75
157
|
// home: a task session it created by its stored origin, any other
|
|
76
158
|
// session by an origin for this send only.
|
|
77
159
|
// Forwarded files ride along as stored attachment parts (workspace-tools.mjs).
|
|
78
|
-
const prompt = handoffContent(args.prompt, context.attachmentParts);
|
|
79
|
-
|
|
160
|
+
const prompt = handoffContent(room ? room.text : args.prompt, context.attachmentParts);
|
|
161
|
+
const turnSessionId = room ? room.sessionId : sessionId;
|
|
162
|
+
const from = { sessionId: caller, ...(context.callerAgentId ? { agentId: context.callerAgentId } : {}) };
|
|
163
|
+
// The member's turn carries the send's hop (`transcriptMeta.hop`), so a
|
|
164
|
+
// chain through the room counts on toward MAX_BOT_HOPS.
|
|
165
|
+
const submit = room
|
|
166
|
+
? async ({ id, onSettled, follow, transcriptMeta } = {}) => ({
|
|
167
|
+
accepted:
|
|
168
|
+
(await room.host.ask(sessionId, {
|
|
169
|
+
agentId: room.agentId,
|
|
170
|
+
prompt,
|
|
171
|
+
from,
|
|
172
|
+
...(id ? { id } : {}),
|
|
173
|
+
...(transcriptMeta?.hop > 0 ? { hop: transcriptMeta.hop } : {}),
|
|
174
|
+
...(onSettled ? { onSettled } : {}),
|
|
175
|
+
...(follow ? { follow } : {}),
|
|
176
|
+
})) === true,
|
|
177
|
+
})
|
|
178
|
+
: null;
|
|
179
|
+
const addressed = room ? { to: room.agentId } : {};
|
|
180
|
+
let origin = await agentTasks.reportOrigin(turnSessionId, context.callerAgentId, caller);
|
|
80
181
|
// A send made in a bot-descended turn (`callerHop` > 0), or by a bot to
|
|
81
182
|
// another session, extends the chain by one; a bot's own task session
|
|
82
183
|
// only continues it. Refused past the limit, whatever the target.
|
|
@@ -91,20 +192,25 @@ export function createSessionControl({
|
|
|
91
192
|
// The task session a bot created acts as the sending conversation does
|
|
92
193
|
// from now on (attended chat or run); another's session keeps its own.
|
|
93
194
|
if (origin.target !== 'session') await rebindDelegation(sessionId, caller);
|
|
94
|
-
const sent = await agentTasks.send(
|
|
95
|
-
return { sessionId, ...sent, ...(hop > 0 ? { hop } : {}) };
|
|
195
|
+
const sent = await agentTasks.send(turnSessionId, prompt, origin, submit);
|
|
196
|
+
return { sessionId, ...addressed, ...sent, ...(hop > 0 ? { hop } : {}) };
|
|
96
197
|
}
|
|
97
198
|
const hop = turnHop > 0 ? chained(turnHop + 1) : 0;
|
|
98
|
-
|
|
99
|
-
await
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
199
|
+
let accepted;
|
|
200
|
+
if (submit) accepted = (await submit(hop > 0 ? { transcriptMeta: { hop } } : {})).accepted;
|
|
201
|
+
else {
|
|
202
|
+
// New work from this sender: a delegated target acts as the sender does.
|
|
203
|
+
await rebindDelegation(sessionId, caller);
|
|
204
|
+
const result = await service.submitSession({
|
|
205
|
+
sessionId,
|
|
206
|
+
prompt,
|
|
207
|
+
options: { mode: 'prompt', transcriptMeta: { sender: 'lead', ...(hop > 0 ? { hop } : {}) } },
|
|
208
|
+
});
|
|
209
|
+
accepted = result.accepted === true;
|
|
210
|
+
}
|
|
106
211
|
return {
|
|
107
212
|
sessionId,
|
|
213
|
+
...addressed,
|
|
108
214
|
accepted,
|
|
109
215
|
reportsBack: false,
|
|
110
216
|
...(accepted ? { reportsBackReason: NOT_BOT_REASON } : {}),
|
|
@@ -112,6 +218,17 @@ export function createSessionControl({
|
|
|
112
218
|
};
|
|
113
219
|
}
|
|
114
220
|
if (action === 'cancel') {
|
|
221
|
+
// A room's exchange is run by the desktop room host, so its stop (the UI
|
|
222
|
+
// stop button) ends it; the log is not a session the service can abort.
|
|
223
|
+
if (roomIdOfSession(sessionId)) {
|
|
224
|
+
let host;
|
|
225
|
+
try {
|
|
226
|
+
host = agentRoomHost();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
throw new Error(`session: cannot cancel ${sessionId}: ${error.message}`);
|
|
229
|
+
}
|
|
230
|
+
return { sessionId, aborted: host.stop(sessionId) === true };
|
|
231
|
+
}
|
|
115
232
|
const result = await service.abortSession({ sessionId });
|
|
116
233
|
return { sessionId, aborted: result.aborted === true };
|
|
117
234
|
}
|
|
@@ -184,6 +184,10 @@ export function createDaemonTelemetry({
|
|
|
184
184
|
log = () => {},
|
|
185
185
|
getWork = () => ({}),
|
|
186
186
|
onInterval = null,
|
|
187
|
+
// Runs once when the loop starts, before its first window: a probe that
|
|
188
|
+
// judges each window (the lag profiler) arms here so the first one covers
|
|
189
|
+
// boot instead of starting only at the first tick.
|
|
190
|
+
onStart = null,
|
|
187
191
|
now,
|
|
188
192
|
pid,
|
|
189
193
|
memoryUsage,
|
|
@@ -239,6 +243,11 @@ export function createDaemonTelemetry({
|
|
|
239
243
|
}, ms);
|
|
240
244
|
handle?.unref?.();
|
|
241
245
|
timer = handle;
|
|
246
|
+
try {
|
|
247
|
+
onStart?.();
|
|
248
|
+
} catch {
|
|
249
|
+
/* a start probe must not stop the sample loop */
|
|
250
|
+
}
|
|
242
251
|
return timer;
|
|
243
252
|
}
|
|
244
253
|
|
|
@@ -359,6 +359,11 @@ slowSpans.setLog(log);
|
|
|
359
359
|
daemonTelemetry = createDaemonTelemetry({
|
|
360
360
|
log,
|
|
361
361
|
getWork: inFlightWork,
|
|
362
|
+
// Arms the profiler at daemon-ready when lag-profile.on exists (a no-op
|
|
363
|
+
// without it), so the first window holds the startup stalls.
|
|
364
|
+
onStart() {
|
|
365
|
+
void lagProfiler.tick();
|
|
366
|
+
},
|
|
362
367
|
onInterval() {
|
|
363
368
|
// eventLoopDelay is reset at the end of every tick, so this status is the
|
|
364
369
|
// lag of the 30s window that just ended — the same window the lag
|
|
@@ -41,13 +41,22 @@ import {
|
|
|
41
41
|
createTranscriptSpillBuffer,
|
|
42
42
|
sweepStaleTranscriptSpillDirsOnce,
|
|
43
43
|
} from '../tui/session/transcript-spill.mjs';
|
|
44
|
+
import { importInStages } from './staged-import.mjs';
|
|
44
45
|
|
|
45
46
|
const SESSION_RUNTIME_MODULE = '../mixdog-session-runtime.mjs';
|
|
47
|
+
// Its one module (only mixdog-session-runtime.mjs imports it): its boot
|
|
48
|
+
// modules are the graph's stages.
|
|
49
|
+
const SESSION_RUNTIME_CORE = '../session-runtime/runtime-core.mjs';
|
|
50
|
+
const importSessionRuntimeInStages = () =>
|
|
51
|
+
importInStages(
|
|
52
|
+
new URL(SESSION_RUNTIME_MODULE, import.meta.url).href,
|
|
53
|
+
new URL(SESSION_RUNTIME_CORE, import.meta.url).href
|
|
54
|
+
);
|
|
46
55
|
|
|
47
56
|
// Shared across create calls so concurrent session startups import the
|
|
48
57
|
// runtime graph once.
|
|
49
58
|
let sessionRuntimeModulePromise = null;
|
|
50
|
-
let sessionRuntimeImporter =
|
|
59
|
+
let sessionRuntimeImporter = importSessionRuntimeInStages;
|
|
51
60
|
function importSessionRuntimeModule() {
|
|
52
61
|
// A failed import is forgotten so the next caller retries lazily.
|
|
53
62
|
sessionRuntimeModulePromise ??= Promise.resolve()
|
|
@@ -79,7 +88,7 @@ export async function warmLocalSessionRuntime() {
|
|
|
79
88
|
|
|
80
89
|
/** Test seam: replace the dynamic import of the session-runtime module. */
|
|
81
90
|
export function setSessionRuntimeImporterForTest(importer) {
|
|
82
|
-
sessionRuntimeImporter = importer ||
|
|
91
|
+
sessionRuntimeImporter = importer || importSessionRuntimeInStages;
|
|
83
92
|
sessionRuntimeModulePromise = null;
|
|
84
93
|
}
|
|
85
94
|
|
|
@@ -57,6 +57,8 @@ export function createInlineBootLoaders({
|
|
|
57
57
|
if (isClosed()) return false;
|
|
58
58
|
try {
|
|
59
59
|
const module = await loadLocalModule();
|
|
60
|
+
// A loop turn between the two graph loads: one piece held the loop.
|
|
61
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
60
62
|
if (isClosed()) return false;
|
|
61
63
|
return (await module.warmLocalSessionRuntime?.()) !== false;
|
|
62
64
|
} catch {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// A bot's saved model route reaches the chats it already has: its home thread
|
|
2
|
+
// and its group-room member sessions, from their next turn (the session route
|
|
3
|
+
// API's deferred pick, the one every model picker uses: a running turn finishes
|
|
4
|
+
// on the model it started on). The latest saved route wins. A thread no
|
|
5
|
+
// runtime hosts keeps its model until it is opened (the desktop refreshes it
|
|
6
|
+
// then, agent-home-sessions.ts).
|
|
7
|
+
import { subscribeAgentRouteChanges } from '../../session-runtime/agent-profile-changes.mjs';
|
|
8
|
+
import { agentHomeSessionId } from '../../runtime/agent/orchestrator/session/agent-home.mjs';
|
|
9
|
+
import { roomMemberOfSession } from '../../runtime/agent/orchestrator/session/agent-room.mjs';
|
|
10
|
+
|
|
11
|
+
const ROUTE_KEYS = [
|
|
12
|
+
'provider',
|
|
13
|
+
'model',
|
|
14
|
+
'effort',
|
|
15
|
+
'fast',
|
|
16
|
+
'autoEffort',
|
|
17
|
+
'modelParameters',
|
|
18
|
+
'contextPercent',
|
|
19
|
+
'contextTokens',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/** The pick the session route API takes for a saved route. */
|
|
23
|
+
function sessionRoutePick(route) {
|
|
24
|
+
const pick = { sessionOnly: true };
|
|
25
|
+
for (const key of ROUTE_KEYS) if (route?.[key] !== undefined && route[key] !== null) pick[key] = route[key];
|
|
26
|
+
return pick;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createAgentRouteFollow({ sessionsById, sessionOwner, advance, publishStep, log }) {
|
|
30
|
+
// Each chat takes its updates one at a time, in the order they were
|
|
31
|
+
// published: a slow chat never lets an older route land after a newer one.
|
|
32
|
+
const tails = new Map(); // sessionId -> Promise of its last queued update
|
|
33
|
+
|
|
34
|
+
function apply(sessionId, pick) {
|
|
35
|
+
const entry = sessionOwner(sessionId);
|
|
36
|
+
if (typeof entry?.runtime?.setRoute !== 'function') return Promise.resolve();
|
|
37
|
+
return Promise.resolve()
|
|
38
|
+
.then(async () => {
|
|
39
|
+
await entry.runtime.setRoute(pick);
|
|
40
|
+
const step = advance(entry);
|
|
41
|
+
if (step.changed) publishStep(entry, step);
|
|
42
|
+
})
|
|
43
|
+
.catch((error) => {
|
|
44
|
+
// A route the runtime refuses (provider signed out) leaves the chat on its model.
|
|
45
|
+
log(`agent route change not applied to ${sessionId}: ${error?.message || error}`);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function enqueue(sessionId, pick) {
|
|
50
|
+
const queued = (tails.get(sessionId) || Promise.resolve()).then(() => apply(sessionId, pick));
|
|
51
|
+
tails.set(sessionId, queued);
|
|
52
|
+
queued.then(() => {
|
|
53
|
+
if (tails.get(sessionId) === queued) tails.delete(sessionId);
|
|
54
|
+
});
|
|
55
|
+
return queued;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Every matching chat is queued before the first await: publications queue in order.
|
|
59
|
+
function follow(agentId, route) {
|
|
60
|
+
const home = agentHomeSessionId(agentId);
|
|
61
|
+
if (!home || !route?.provider || !route?.model) return Promise.resolve();
|
|
62
|
+
const pick = sessionRoutePick(route);
|
|
63
|
+
const queued = [...sessionsById.keys()]
|
|
64
|
+
.filter((sessionId) => sessionId === home || roomMemberOfSession(sessionId)?.agentId === agentId)
|
|
65
|
+
.map((sessionId) => enqueue(sessionId, pick));
|
|
66
|
+
return Promise.all(queued);
|
|
67
|
+
}
|
|
68
|
+
return subscribeAgentRouteChanges(follow);
|
|
69
|
+
}
|
|
@@ -62,15 +62,43 @@ export function createSessionTurnCalls(ctx) {
|
|
|
62
62
|
return step;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
// Intakes in flight per session: an abort of the session also cancels an
|
|
66
|
+
// attachment conversion that has not yet become a turn.
|
|
67
|
+
const intakeAborts = new Map();
|
|
68
|
+
|
|
69
|
+
async function submitSession({
|
|
70
|
+
sessionId,
|
|
71
|
+
prompt,
|
|
72
|
+
options = {},
|
|
73
|
+
open: openHints = {},
|
|
74
|
+
baseRevision = null,
|
|
75
|
+
signal = null,
|
|
76
|
+
} = {}) {
|
|
66
77
|
const id = String(sessionId || '');
|
|
67
78
|
if (!id) throw new TypeError('sessionId is required');
|
|
68
79
|
const entry = await entryForSession(id, openHints || {});
|
|
69
80
|
const target = entry.runtime.submitAsync;
|
|
70
81
|
if (typeof target !== 'function') throw new TypeError('session runtime must implement submitAsync');
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
const intakeAbort = new AbortController();
|
|
83
|
+
const forwardAbort = () => intakeAbort.abort(signal.reason);
|
|
84
|
+
if (signal?.aborted) forwardAbort();
|
|
85
|
+
else signal?.addEventListener?.('abort', forwardAbort, { once: true });
|
|
86
|
+
if (!intakeAborts.has(id)) intakeAborts.set(id, new Set());
|
|
87
|
+
intakeAborts.get(id).add(intakeAbort);
|
|
88
|
+
let intake;
|
|
89
|
+
try {
|
|
90
|
+
intake = await preparePromptSubmissionForProvider(
|
|
91
|
+
materializePromptSubmission(prompt, options || {}),
|
|
92
|
+
{ nativePdf: providerNativePdf(entry.runtime.provider || entry.runtime.session?.provider || '') },
|
|
93
|
+
{ signal: intakeAbort.signal }
|
|
94
|
+
);
|
|
95
|
+
} finally {
|
|
96
|
+
signal?.removeEventListener?.('abort', forwardAbort);
|
|
97
|
+
intakeAborts.get(id)?.delete(intakeAbort);
|
|
98
|
+
if (!intakeAborts.get(id)?.size) intakeAborts.delete(id);
|
|
99
|
+
}
|
|
100
|
+
// A cancel that landed after the last conversion step must not submit.
|
|
101
|
+
intakeAbort.signal.throwIfAborted();
|
|
74
102
|
// Await intake only: submitAsync resolves once the prompt is represented by
|
|
75
103
|
// the queue/user row, while provider execution remains daemon-owned and
|
|
76
104
|
// detached.
|
|
@@ -131,6 +159,7 @@ export function createSessionTurnCalls(ctx) {
|
|
|
131
159
|
async function abortSession({ sessionId, open: openHints = {}, options = {}, baseRevision = null } = {}) {
|
|
132
160
|
const id = String(sessionId || '');
|
|
133
161
|
if (!id) throw new TypeError('sessionId is required');
|
|
162
|
+
for (const intakeAbort of intakeAborts.get(id) ?? []) intakeAbort.abort();
|
|
134
163
|
const entry = await entryForSession(id, openHints || {});
|
|
135
164
|
const target = entry.runtime.abort;
|
|
136
165
|
if (typeof target !== 'function') throw new TypeError('session action abort is unavailable');
|