mixdog 0.9.68 → 0.9.69
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 +6 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -6
- package/src/runtime/agent/orchestrator/context/collect.mjs +42 -27
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +0 -6
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +161 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +8 -204
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +25 -13
- package/src/runtime/agent/orchestrator/session/manager.mjs +0 -11
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +613 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +9 -575
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +41 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +154 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +78 -18
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +9 -144
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -3
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +24 -8
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +6 -2
- package/src/runtime/channels/lib/webhook.mjs +13 -1
- package/src/session-runtime/memory-daemon-probe.mjs +61 -0
- package/src/session-runtime/model-capabilities.mjs +6 -4
- package/src/session-runtime/notification-bus.mjs +82 -0
- package/src/session-runtime/provider-auth-api.mjs +16 -1
- package/src/session-runtime/provider-usage.mjs +3 -0
- package/src/session-runtime/remote-control.mjs +166 -0
- package/src/session-runtime/remote-transcript.mjs +126 -0
- package/src/session-runtime/remote-transition-queue.mjs +14 -0
- package/src/session-runtime/runtime-core.mjs +143 -705
- package/src/session-runtime/runtime-tunables.mjs +57 -0
- package/src/session-runtime/self-update.mjs +129 -0
- package/src/session-runtime/skills-api.mjs +77 -0
- package/src/session-runtime/tool-surface.mjs +83 -0
- package/src/session-runtime/workflow-agents-api.mjs +206 -3
- package/src/session-runtime/workflow.mjs +84 -17
- package/src/standalone/agent-tool/worker-index.mjs +287 -0
- package/src/standalone/agent-tool.mjs +22 -346
- package/src/standalone/agent-watchdog-registry.mjs +101 -0
- package/src/standalone/channel-worker.mjs +7 -13
- package/src/tui/components/StatusLine.jsx +6 -5
- package/src/tui/dist/index.mjs +124 -94
- package/src/tui/engine/labels.mjs +0 -8
- package/src/tui/engine/session-api-ext.mjs +17 -8
- package/src/tui/engine/turn-watchdog.mjs +92 -0
- package/src/tui/engine/turn.mjs +18 -70
- package/src/workflows/default/WORKFLOW.md +1 -1
- package/src/workflows/solo/WORKFLOW.md +1 -1
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
// keep this module free of the runtime's path/provider constants.
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
5
|
import { existsSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
6
7
|
import { clean } from './session-text.mjs';
|
|
7
8
|
import { normalizeEffortInput } from './effort.mjs';
|
|
8
9
|
import { isLikelyRawModelId } from './config-helpers.mjs';
|
|
9
10
|
import { readTextSafe, readJsonSafe } from './fs-utils.mjs';
|
|
11
|
+
import { isHiddenAgent } from '../runtime/agent/orchestrator/internal-agents.mjs';
|
|
10
12
|
|
|
11
13
|
export const WORKFLOW_ROUTE_SLOTS = ['lead', 'agent', 'explorer', 'memory'];
|
|
12
14
|
export const FIXED_AGENT_SLOTS = Object.freeze([
|
|
@@ -42,6 +44,18 @@ function setAgentDefinitionCache(key, value) {
|
|
|
42
44
|
agentDefinitionCache.set(key, value);
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
// Editor writes must invalidate the definition cache or a saved AGENT.md
|
|
48
|
+
// stays stale for the session lifetime (keys are `${dir}\n${agentId}`).
|
|
49
|
+
export function clearAgentDefinitionCache(agentId = '') {
|
|
50
|
+
if (!agentId) {
|
|
51
|
+
agentDefinitionCache.clear();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const key of [...agentDefinitionCache.keys()]) {
|
|
55
|
+
if (key.endsWith(`\n${agentId}`)) agentDefinitionCache.delete(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
export function workflowPresetId(slot) {
|
|
46
60
|
return `workflow-${slot}`;
|
|
47
61
|
}
|
|
@@ -69,6 +83,41 @@ export function normalizeWorkflowId(value, fallback = '') {
|
|
|
69
83
|
return /^[a-z0-9][a-z0-9_.-]*$/.test(id) ? id : fallback;
|
|
70
84
|
}
|
|
71
85
|
|
|
86
|
+
function internalIdFromName(value, fallbackPrefix) {
|
|
87
|
+
const name = clean(value);
|
|
88
|
+
const readable = name
|
|
89
|
+
.normalize('NFKD')
|
|
90
|
+
.replace(/\p{M}+/gu, '')
|
|
91
|
+
.toLowerCase()
|
|
92
|
+
.replace(/[^a-z0-9_.-]+/g, '-')
|
|
93
|
+
.replace(/-+/g, '-')
|
|
94
|
+
.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, '');
|
|
95
|
+
const id = normalizeWorkflowId(readable, '');
|
|
96
|
+
if (id) return id;
|
|
97
|
+
return `${fallbackPrefix}-${createHash('sha256').update(name).digest('hex').slice(0, 10)}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function workflowIdFromName(value) {
|
|
101
|
+
return internalIdFromName(value, 'workflow');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function agentIdFromName(value) {
|
|
105
|
+
return internalIdFromName(value, 'agent');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function availableWorkflowId(baseId, isTaken) {
|
|
109
|
+
const id = normalizeWorkflowId(baseId, '');
|
|
110
|
+
if (!id) return '';
|
|
111
|
+
if (!isTaken(id)) return id;
|
|
112
|
+
let suffix = 2;
|
|
113
|
+
while (isTaken(`${id}-${suffix}`)) suffix += 1;
|
|
114
|
+
return `${id}-${suffix}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function availableAgentId(name, isTaken) {
|
|
118
|
+
return availableWorkflowId(agentIdFromName(name), isTaken);
|
|
119
|
+
}
|
|
120
|
+
|
|
72
121
|
// A workflow/agent pack loader is created per data/root layout via
|
|
73
122
|
// createWorkflowHelpers, and the config-aware route helpers via
|
|
74
123
|
// createWorkflowRouteHelpers.
|
|
@@ -87,6 +136,29 @@ export function createWorkflowHelpers({ rootDir, dataDir, readMarkdownDocument,
|
|
|
87
136
|
];
|
|
88
137
|
}
|
|
89
138
|
|
|
139
|
+
// Custom agents (user-authored roles beyond FIXED_AGENT_SLOTS): every
|
|
140
|
+
// agents/<id>/AGENT.md directory whose id is not a fixed role. Mixdog-managed
|
|
141
|
+
// hidden roles (defaults/agents.json: scheduler-task, webhook-handler, …) ship
|
|
142
|
+
// the same directory layout, so they are excluded here — at the data source —
|
|
143
|
+
// instead of per-surface, or the TUI /agents list exposes internal roles the
|
|
144
|
+
// desktop UI happens to filter out.
|
|
145
|
+
function listCustomAgentIds(dir) {
|
|
146
|
+
const ids = new Set();
|
|
147
|
+
for (const root of [join(dir || dataDir, 'agents'), join(rootDir, 'agents')]) {
|
|
148
|
+
if (!existsSync(root)) continue;
|
|
149
|
+
let entries = [];
|
|
150
|
+
try { entries = readdirSync(root, { withFileTypes: true }); } catch { entries = []; }
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (!entry.isDirectory()) continue;
|
|
153
|
+
const id = normalizeWorkflowId(entry.name);
|
|
154
|
+
if (!id || AGENT_ROLE_IDS.has(id) || isHiddenAgent(id)) continue;
|
|
155
|
+
if (!existsSync(join(root, entry.name, 'AGENT.md'))) continue;
|
|
156
|
+
ids.add(id);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return [...ids].sort();
|
|
160
|
+
}
|
|
161
|
+
|
|
90
162
|
function readWorkflowPackFromDir(dir, source = 'built-in', dirName = '') {
|
|
91
163
|
const entry = 'WORKFLOW.md';
|
|
92
164
|
const doc = readMarkdownDocument(readTextSafe(join(dir, entry)));
|
|
@@ -184,21 +256,10 @@ export function createWorkflowHelpers({ rootDir, dataDir, readMarkdownDocument,
|
|
|
184
256
|
setAgentDefinitionCache(cacheKey, definition);
|
|
185
257
|
return definition;
|
|
186
258
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
const definition = {
|
|
193
|
-
id: agentId,
|
|
194
|
-
name: FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
|
|
195
|
-
description: '',
|
|
196
|
-
permission: normalizeAgentPermissionOrNone(legacyDoc.frontmatter.permission),
|
|
197
|
-
frontmatter: legacyDoc.frontmatter,
|
|
198
|
-
body: legacyDoc.body,
|
|
199
|
-
};
|
|
200
|
-
setAgentDefinitionCache(cacheKey, definition);
|
|
201
|
-
return definition;
|
|
259
|
+
// Every shipped and user role lives at agents/<id>/AGENT.md; there is no
|
|
260
|
+
// flat agents/<id>.md layout left to fall back to.
|
|
261
|
+
setAgentDefinitionCache(cacheKey, null);
|
|
262
|
+
return null;
|
|
202
263
|
}
|
|
203
264
|
|
|
204
265
|
function workflowContextBlock(config, dir) {
|
|
@@ -210,7 +271,10 @@ export function createWorkflowHelpers({ rootDir, dataDir, readMarkdownDocument,
|
|
|
210
271
|
const lines = [`# Active Workflow: ${pack.name}`];
|
|
211
272
|
if (pack.description) lines.push(pack.description);
|
|
212
273
|
lines.push(pack.body);
|
|
213
|
-
|
|
274
|
+
// A hand-edited pack may name a hidden role in its `agents:` frontmatter;
|
|
275
|
+
// internal roles are never delegatable, so they never enter the catalog.
|
|
276
|
+
const agentIds = (pack.agentsConfigured ? pack.agents : FIXED_AGENT_SLOTS.map((agent) => agent.id))
|
|
277
|
+
.filter((id) => !isHiddenAgent(id));
|
|
214
278
|
const agentBlocks = agentIds.map((id) => loadAgentDefinition(dir, id)).filter(Boolean);
|
|
215
279
|
if (agentBlocks.length) {
|
|
216
280
|
lines.push('# Available Agents');
|
|
@@ -241,6 +305,7 @@ export function createWorkflowHelpers({ rootDir, dataDir, readMarkdownDocument,
|
|
|
241
305
|
workflowSummary,
|
|
242
306
|
activeWorkflowSummary,
|
|
243
307
|
loadAgentDefinition,
|
|
308
|
+
listCustomAgentIds,
|
|
244
309
|
workflowContextBlock,
|
|
245
310
|
activeWorkflowContext,
|
|
246
311
|
};
|
|
@@ -349,7 +414,9 @@ export function createWorkflowRouteHelpers({ resolveDefaultProvider, findPreset
|
|
|
349
414
|
}
|
|
350
415
|
|
|
351
416
|
function agentRouteFromConfig(config, agentId, _dataDir) {
|
|
352
|
-
|
|
417
|
+
// Custom agents (user-authored roles) pass through as workflow-style ids;
|
|
418
|
+
// their routes live in config.agents[<id>] like the fixed roles.
|
|
419
|
+
const id = normalizeAgentId(agentId) || normalizeWorkflowId(agentId);
|
|
353
420
|
if (!id) return null;
|
|
354
421
|
// Read/interpret path: inject config.defaultProvider (then DEFAULT_PROVIDER)
|
|
355
422
|
// when a stored route omits its provider.
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Worker-index persistence for the agent tool: the on-disk row store (tag →
|
|
2
|
+
// session), its mtime-keyed parse cache, the batched atomic writer and the
|
|
3
|
+
// tag-map projection. Extracted from agent-tool.mjs, which now owns spawn /
|
|
4
|
+
// dispatch flow only. The tag Maps are passed in by reference so the facade
|
|
5
|
+
// and this store observe the same live state.
|
|
6
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
|
|
9
|
+
import { updateJsonAtomicSync } from '../../runtime/shared/atomic-file.mjs';
|
|
10
|
+
import { resolveAgentTerminalReapMs } from '../../session-runtime/config-helpers.mjs';
|
|
11
|
+
import { WORKER_INDEX_FILE } from './tool-def.mjs';
|
|
12
|
+
import { agentTagOf, clean, positiveInt, rowMatchesContext } from './helpers.mjs';
|
|
13
|
+
import {
|
|
14
|
+
applyWorkerRowUpsert,
|
|
15
|
+
isTerminalWorkerStatus,
|
|
16
|
+
normalizeTagTombstones,
|
|
17
|
+
tagTombstoneKey,
|
|
18
|
+
workerRowKey,
|
|
19
|
+
workerRowTime,
|
|
20
|
+
} from './worker-rows.mjs';
|
|
21
|
+
|
|
22
|
+
export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCwds }) {
|
|
23
|
+
const pendingMutators = [];
|
|
24
|
+
let flushTimer = null;
|
|
25
|
+
// Mtime-keyed parse cache. A single spawn calls refreshTagsFromSessions /
|
|
26
|
+
// resolveTag / nextTag, which each re-read and re-parse this file; across a
|
|
27
|
+
// parallel fanout that is O(spawns^2) synchronous reads of the same bytes.
|
|
28
|
+
let cache = null; // { mtimeMs, size, rows, tombstones }
|
|
29
|
+
let cacheDirty = true;
|
|
30
|
+
|
|
31
|
+
function workerIndexPath() {
|
|
32
|
+
return dataDir ? resolve(dataDir, WORKER_INDEX_FILE) : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A terminal row ages out after the configured reap window so the index does
|
|
36
|
+
// not grow without bound; live rows are always kept.
|
|
37
|
+
function keepWorkerRow(row = {}) {
|
|
38
|
+
if (!clean(row.tag) || !clean(row.sessionId)) return false;
|
|
39
|
+
const t = workerRowTime(row);
|
|
40
|
+
if (!t) return true;
|
|
41
|
+
if (!isTerminalWorkerStatus(row.status || row.stage)) return true;
|
|
42
|
+
const reapMs = resolveAgentTerminalReapMs(cfgMod.loadConfig(), row.provider);
|
|
43
|
+
return reapMs == null || Date.now() - t < reapMs;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeWorkerRows(value) {
|
|
47
|
+
const source = Array.isArray(value?.workers)
|
|
48
|
+
? value.workers
|
|
49
|
+
: (value?.workers && typeof value.workers === 'object'
|
|
50
|
+
? Object.values(value.workers)
|
|
51
|
+
: (Array.isArray(value) ? value : []));
|
|
52
|
+
return source
|
|
53
|
+
.filter((row) => row && typeof row === 'object')
|
|
54
|
+
.map((row) => ({
|
|
55
|
+
tag: clean(row.tag),
|
|
56
|
+
sessionId: clean(row.sessionId),
|
|
57
|
+
agent: clean(row.agent) || null,
|
|
58
|
+
provider: clean(row.provider) || null,
|
|
59
|
+
model: clean(row.model) || null,
|
|
60
|
+
preset: clean(row.preset) || null,
|
|
61
|
+
effort: clean(row.effort) || null,
|
|
62
|
+
fast: row.fast === true ? true : (row.fast === false ? false : null),
|
|
63
|
+
status: clean(row.status) || 'idle',
|
|
64
|
+
stage: clean(row.stage) || clean(row.status) || 'idle',
|
|
65
|
+
createdAt: clean(row.createdAt) || null,
|
|
66
|
+
updatedAt: clean(row.updatedAt) || null,
|
|
67
|
+
lastUsedAt: clean(row.lastUsedAt) || null,
|
|
68
|
+
finishedAt: clean(row.finishedAt) || null,
|
|
69
|
+
clientHostPid: positiveInt(row.clientHostPid),
|
|
70
|
+
cwd: clean(row.cwd) || null,
|
|
71
|
+
task_id: clean(row.task_id || row.taskId) || null,
|
|
72
|
+
error: clean(row.error) || null,
|
|
73
|
+
permission: clean(row.permission) || null,
|
|
74
|
+
toolPermission: clean(row.toolPermission) || null,
|
|
75
|
+
messages: positiveInt(row.messages) || 0,
|
|
76
|
+
tools: positiveInt(row.tools) || 0,
|
|
77
|
+
}))
|
|
78
|
+
.filter(keepWorkerRow);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function invalidateCache() {
|
|
82
|
+
cacheDirty = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readAllWorkerRows() {
|
|
86
|
+
const file = workerIndexPath();
|
|
87
|
+
if (!file) return [];
|
|
88
|
+
let st = null;
|
|
89
|
+
try { st = statSync(file); } catch { cache = null; return []; }
|
|
90
|
+
if (!cacheDirty && cache && cache.mtimeMs === st.mtimeMs && cache.size === st.size) {
|
|
91
|
+
return cache.rows;
|
|
92
|
+
}
|
|
93
|
+
let rows = [];
|
|
94
|
+
let tombstones = [];
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
97
|
+
rows = normalizeWorkerRows(parsed);
|
|
98
|
+
tombstones = normalizeTagTombstones(parsed);
|
|
99
|
+
} catch {
|
|
100
|
+
rows = [];
|
|
101
|
+
tombstones = [];
|
|
102
|
+
}
|
|
103
|
+
cache = { mtimeMs: st.mtimeMs, size: st.size, rows, tombstones };
|
|
104
|
+
cacheDirty = false;
|
|
105
|
+
return rows;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readAllTagTombstones() {
|
|
109
|
+
readAllWorkerRows();
|
|
110
|
+
return cache?.tombstones || [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readTagTombstones(context = {}) {
|
|
114
|
+
return readAllTagTombstones().filter((row) => rowMatchesContext(row, context));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readWorkerRows(context = {}) {
|
|
118
|
+
const rows = readAllWorkerRows();
|
|
119
|
+
if (rows.length === 0) return rows;
|
|
120
|
+
return rows.filter((row) => rowMatchesContext(row, context));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Single writer path: every mutation re-reads under the file lock, applies
|
|
124
|
+
// the caller's mutator over keyed maps, then republishes rows + tombstones.
|
|
125
|
+
function writeWorkerRows(mutator) {
|
|
126
|
+
const file = workerIndexPath();
|
|
127
|
+
if (!file || typeof mutator !== 'function') return null;
|
|
128
|
+
try {
|
|
129
|
+
const result = updateJsonAtomicSync(file, (cur) => {
|
|
130
|
+
const byKey = new Map();
|
|
131
|
+
for (const row of normalizeWorkerRows(cur)) {
|
|
132
|
+
const key = workerRowKey(row);
|
|
133
|
+
if (key) byKey.set(key, row);
|
|
134
|
+
}
|
|
135
|
+
const tombstonesByKey = new Map();
|
|
136
|
+
for (const row of normalizeTagTombstones(cur, { cap: false })) {
|
|
137
|
+
tombstonesByKey.set(tagTombstoneKey(row), row);
|
|
138
|
+
}
|
|
139
|
+
const priorityTombstoneKeys = new Set();
|
|
140
|
+
mutator(byKey, tombstonesByKey, priorityTombstoneKeys);
|
|
141
|
+
const workers = {};
|
|
142
|
+
for (const row of [...byKey.values()].filter(keepWorkerRow)) {
|
|
143
|
+
const key = workerRowKey(row);
|
|
144
|
+
if (key) workers[key] = row;
|
|
145
|
+
}
|
|
146
|
+
const tombstones = {};
|
|
147
|
+
for (const row of normalizeTagTombstones(
|
|
148
|
+
{ tombstones: [...tombstonesByKey.values()] },
|
|
149
|
+
{ priorityKeys: priorityTombstoneKeys },
|
|
150
|
+
)) {
|
|
151
|
+
tombstones[tagTombstoneKey(row)] = row;
|
|
152
|
+
}
|
|
153
|
+
return { version: 2, updatedAt: new Date().toISOString(), workers, tombstones };
|
|
154
|
+
}, { lock: true });
|
|
155
|
+
// This process just rewrote the index; force the next read to re-parse
|
|
156
|
+
// even if the new mtime/size happen to collide with the cached stat.
|
|
157
|
+
invalidateCache();
|
|
158
|
+
return result;
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Spawn-path writes are batched onto one microtask so a parallel fanout pays
|
|
165
|
+
// a single locked rewrite instead of one per worker.
|
|
166
|
+
function flushWorkerIndexMutations() {
|
|
167
|
+
if (flushTimer) {
|
|
168
|
+
try { clearImmediate(flushTimer); } catch { /* already fired */ }
|
|
169
|
+
flushTimer = null;
|
|
170
|
+
}
|
|
171
|
+
if (pendingMutators.length === 0) return;
|
|
172
|
+
const batch = pendingMutators.splice(0, pendingMutators.length);
|
|
173
|
+
writeWorkerRows((byKey) => {
|
|
174
|
+
for (const mutator of batch) {
|
|
175
|
+
try { mutator(byKey); } catch { /* one bad row never drops the batch */ }
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function queueWorkerIndexMutation(mutator) {
|
|
181
|
+
if (typeof mutator !== 'function') return false;
|
|
182
|
+
if (!workerIndexPath()) return false;
|
|
183
|
+
pendingMutators.push(mutator);
|
|
184
|
+
if (!flushTimer) {
|
|
185
|
+
flushTimer = setImmediate(flushWorkerIndexMutations);
|
|
186
|
+
flushTimer.unref?.();
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function workerRowFromSession(session, fallbackTag = '', extra = {}) {
|
|
192
|
+
const tag = agentTagOf(session) || clean(fallbackTag) || clean(extra.tag);
|
|
193
|
+
const sessionId = clean(session?.id || extra.sessionId);
|
|
194
|
+
if (!tag || !sessionId) return null;
|
|
195
|
+
const runtime = mgr.getSessionRuntime?.(sessionId);
|
|
196
|
+
const status = clean(extra.status) || (session?.closed === true ? 'closed' : clean(session?.status) || 'idle');
|
|
197
|
+
const stage = clean(extra.stage) || clean(runtime?.stage) || status;
|
|
198
|
+
const nowIso = new Date().toISOString();
|
|
199
|
+
return {
|
|
200
|
+
tag,
|
|
201
|
+
sessionId,
|
|
202
|
+
agent: clean(extra.agent) || clean(session?.agent) || null,
|
|
203
|
+
provider: clean(extra.provider) || clean(session?.provider) || null,
|
|
204
|
+
model: clean(extra.model) || clean(session?.model) || null,
|
|
205
|
+
preset: clean(extra.preset) || clean(session?.presetName) || null,
|
|
206
|
+
effort: clean(extra.effort) || clean(session?.effort) || null,
|
|
207
|
+
fast: extra.fast === true || extra.fast === false ? extra.fast : (session?.fast === true ? true : null),
|
|
208
|
+
status,
|
|
209
|
+
stage,
|
|
210
|
+
createdAt: clean(session?.createdAt) || clean(extra.createdAt) || nowIso,
|
|
211
|
+
updatedAt: clean(extra.updatedAt) || nowIso,
|
|
212
|
+
lastUsedAt: clean(session?.lastUsedAt) || null,
|
|
213
|
+
finishedAt: clean(extra.finishedAt) || null,
|
|
214
|
+
clientHostPid: positiveInt(extra.clientHostPid) || positiveInt(session?.clientHostPid),
|
|
215
|
+
cwd: clean(session?.cwd) || clean(extra.cwd) || null,
|
|
216
|
+
task_id: clean(extra.task_id || extra.taskId) || null,
|
|
217
|
+
error: clean(extra.error) || null,
|
|
218
|
+
permission: clean(session?.permission) || null,
|
|
219
|
+
toolPermission: clean(session?.toolPermission) || null,
|
|
220
|
+
messages: Array.isArray(session?.messages) ? session.messages.length : 0,
|
|
221
|
+
tools: Array.isArray(session?.tools) ? session.tools.length : 0,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Every upsert also projects the row into the in-memory tag maps so tag
|
|
226
|
+
// lookups never wait on a disk round-trip.
|
|
227
|
+
function upsertWorkerRow(row, { defer = false } = {}) {
|
|
228
|
+
const normalized = normalizeWorkerRows({ workers: [row] })[0];
|
|
229
|
+
if (!normalized) return false;
|
|
230
|
+
tags.set(normalized.tag, normalized.sessionId);
|
|
231
|
+
if (normalized.agent) tagAgents.set(normalized.tag, normalized.agent);
|
|
232
|
+
if (normalized.cwd) tagCwds.set(normalized.tag, normalized.cwd);
|
|
233
|
+
if (defer) return queueWorkerIndexMutation((byKey) => applyWorkerRowUpsert(byKey, normalized));
|
|
234
|
+
writeWorkerRows((byKey) => { applyWorkerRowUpsert(byKey, normalized); });
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function upsertWorkerSession(session, fallbackTag = '', extra = {}) {
|
|
239
|
+
return upsertWorkerRow(workerRowFromSession(session, fallbackTag, extra));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function upsertWorkerSessionDeferred(session, fallbackTag = '', extra = {}) {
|
|
243
|
+
return upsertWorkerRow(workerRowFromSession(session, fallbackTag, extra), { defer: true });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function removeWorkerRow({ tag = '', sessionId = '' } = {}) {
|
|
247
|
+
const targetTag = clean(tag);
|
|
248
|
+
const targetSessionId = clean(sessionId);
|
|
249
|
+
flushWorkerIndexMutations();
|
|
250
|
+
writeWorkerRows((byKey) => {
|
|
251
|
+
for (const [key, row] of [...byKey.entries()]) {
|
|
252
|
+
if ((targetSessionId && row.sessionId === targetSessionId) || (targetTag && row.tag === targetTag)) {
|
|
253
|
+
byKey.delete(key);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function refreshTagsFromIndex(context = {}) {
|
|
260
|
+
const rows = readWorkerRows(context);
|
|
261
|
+
for (const row of rows) {
|
|
262
|
+
if (!row.tag || !row.sessionId) continue;
|
|
263
|
+
tags.set(row.tag, row.sessionId);
|
|
264
|
+
if (row.agent) tagAgents.set(row.tag, row.agent);
|
|
265
|
+
if (row.cwd) tagCwds.set(row.tag, row.cwd);
|
|
266
|
+
}
|
|
267
|
+
return rows;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
workerIndexPath,
|
|
272
|
+
invalidateWorkerRowsCache: invalidateCache,
|
|
273
|
+
readAllWorkerRows,
|
|
274
|
+
readAllTagTombstones,
|
|
275
|
+
readTagTombstones,
|
|
276
|
+
readWorkerRows,
|
|
277
|
+
writeWorkerRows,
|
|
278
|
+
flushWorkerIndexMutations,
|
|
279
|
+
queueWorkerIndexMutation,
|
|
280
|
+
workerRowFromSession,
|
|
281
|
+
upsertWorkerRow,
|
|
282
|
+
upsertWorkerSession,
|
|
283
|
+
upsertWorkerSessionDeferred,
|
|
284
|
+
removeWorkerRow,
|
|
285
|
+
refreshTagsFromIndex,
|
|
286
|
+
};
|
|
287
|
+
}
|