mixdog 0.9.86 → 0.9.87

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.
Files changed (38) hide show
  1. package/package.json +2 -2
  2. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -2
  3. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +125 -16
  4. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  5. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
  6. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
  7. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -3
  8. package/src/runtime/agent/orchestrator/tools/token-manifest.json +23 -2
  9. package/src/runtime/memory/lib/memory-action-handlers.mjs +4 -1
  10. package/src/runtime/memory/tool-defs.mjs +19 -3
  11. package/src/session-runtime/lifecycle-api.mjs +15 -0
  12. package/src/session-runtime/provider-auth-api.mjs +8 -0
  13. package/src/session-runtime/provider-usage.mjs +30 -5
  14. package/src/session-runtime/runtime-core.mjs +267 -481
  15. package/src/session-runtime/session-lifecycle.mjs +378 -0
  16. package/src/standalone/agent-shard/shard-pool.mjs +6 -5
  17. package/src/standalone/agent-tool/job-views.mjs +329 -0
  18. package/src/standalone/agent-tool/spawn-flow.mjs +629 -0
  19. package/src/standalone/agent-tool/tag-registry.mjs +340 -0
  20. package/src/standalone/agent-tool.mjs +100 -1151
  21. package/src/standalone/usage-dashboard.mjs +23 -1
  22. package/src/tui/App.jsx +362 -2576
  23. package/src/tui/app/app-view.jsx +504 -0
  24. package/src/tui/app/create-app-pickers.mjs +313 -0
  25. package/src/tui/app/prompt-submit.mjs +501 -0
  26. package/src/tui/app/shell-layout.mjs +563 -0
  27. package/src/tui/app/usage-context-panels.mjs +288 -0
  28. package/src/tui/app/use-copy-selection.mjs +56 -0
  29. package/src/tui/app/use-global-key-input.mjs +151 -0
  30. package/src/tui/app/use-pasted-buffers.mjs +115 -0
  31. package/src/tui/app/use-prompt-draft-flow.mjs +167 -0
  32. package/src/tui/app/use-prompt-hint.mjs +58 -0
  33. package/src/tui/app/use-prompt-queue-history.mjs +100 -0
  34. package/src/tui/app/use-terminal-chrome.mjs +72 -0
  35. package/src/tui/app/use-transcript-activity.mjs +131 -0
  36. package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
  37. package/src/tui/dist/index.mjs +9802 -9024
  38. package/src/tui/engine/session-api-ext.mjs +23 -0
@@ -0,0 +1,340 @@
1
+ // Tag registry + terminal-session reaping, extracted from agent-tool.mjs.
2
+ // Owns the tag maps (tag -> sessionId/agent/cwd) and every mutation path:
3
+ // resolve/next/bind/forget, session-index refresh, terminal tombstones, and
4
+ // the delayed reap timers. The maps are returned by reference so the
5
+ // remaining agent-tool closure keeps its original direct reads.
6
+ import { agentTagOf, clean, clearAgentStatuslineRoute, positiveInt, rowMatchesContext, sessionMatchesContext } from './helpers.mjs';
7
+ import { TAG_TOMBSTONE_TTL_MS, isTerminalWorkerStatus, tagTombstoneKey, workerRowTime, workerRowToSession } from './worker-rows.mjs';
8
+ import { resolveAgentTerminalReapMs } from '../../session-runtime/config-helpers.mjs';
9
+ import { createWorkerIndex } from './worker-index.mjs';
10
+ export function createTagRegistry({
11
+ dataDir,
12
+ cfgMod,
13
+ mgr,
14
+ emitSubagentEvent,
15
+ }) {
16
+ const tags = new Map();
17
+ const tagAgents = new Map();
18
+ const tagCwds = new Map();
19
+ const reapTimers = new Map();
20
+ // Worker-index persistence (row store, parse cache, batched atomic writer,
21
+ // tag projection) lives in agent-tool/worker-index.mjs; the tag maps are
22
+ // shared by reference so both sides see the same live state.
23
+ const {
24
+ readAllTagTombstones,
25
+ readTagTombstones,
26
+ readWorkerRows,
27
+ writeWorkerRows,
28
+ flushWorkerIndexMutations,
29
+ upsertWorkerSession,
30
+ upsertWorkerSessionDeferred,
31
+ removeWorkerRow,
32
+ refreshTagsFromIndex,
33
+ } = createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCwds });
34
+
35
+ function wantsSessionScan(args = {}) {
36
+ return args.recover === true || args.scanSessions === true || args.scan_sessions === true;
37
+ }
38
+
39
+ function resolveTag(target, context = {}, options = {}) {
40
+ const scanSessions = options.scanSessions === true;
41
+ const excludeTerminalTraces = options.excludeTerminalTraces === true;
42
+ refreshTagsFromSessions({ scanSessions, context });
43
+ const value = clean(target);
44
+ if (!value) return null;
45
+ if (value.startsWith('sess_')) {
46
+ const session = getLiveSession(value);
47
+ if (session && sessionMatchesContext(session, context)) return value;
48
+ const row = readWorkerRows(context).find((item) => item.sessionId === value);
49
+ return row ? value : null;
50
+ }
51
+ const matches = agentSessionEntries({ scanSessions, context, excludeTerminalTraces })
52
+ .filter((entry) => entry.tag === value);
53
+ if (matches.length === 1) return matches[0].session.id;
54
+ if (matches.length > 1) {
55
+ throw new Error(`agent: tag "${value}" is ambiguous across terminals; use sessionId`);
56
+ }
57
+ const sessionId = tags.get(value) || null;
58
+ const session = getLiveSession(sessionId);
59
+ return session && sessionMatchesContext(session, context) ? sessionId : null;
60
+ }
61
+
62
+ function getLiveSession(sessionId) {
63
+ if (!sessionId) return null;
64
+ const session = mgr.getSession(sessionId);
65
+ return session && session.closed !== true ? session : null;
66
+ }
67
+
68
+ function tagForSession(sessionId) {
69
+ const session = getLiveSession(sessionId);
70
+ const persistedTag = agentTagOf(session);
71
+ if (persistedTag) return persistedTag;
72
+ for (const [tag, sid] of tags.entries()) {
73
+ if (sid === sessionId) return tag;
74
+ }
75
+ return null;
76
+ }
77
+
78
+ function agentSessionEntries({ scanSessions = false, context = {}, excludeTerminalTraces = false } = {}) {
79
+ const rows = [];
80
+ const seen = new Set();
81
+ const add = (session, fallbackTag = '') => {
82
+ const tag = agentTagOf(session) || clean(fallbackTag);
83
+ if (!tag || !session?.id || session.closed === true) return;
84
+ if (!sessionMatchesContext(session, context)) return;
85
+ if (seen.has(session.id)) return;
86
+ seen.add(session.id);
87
+ rows.push({ tag, session });
88
+ };
89
+ const addIndexRow = (row) => {
90
+ const tag = clean(row?.tag);
91
+ const sessionId = clean(row?.sessionId);
92
+ if (!tag || !sessionId || !rowMatchesContext(row, context)) return;
93
+ if (seen.has(sessionId)) return;
94
+ // Collision/resolution enumeration only: a row that is in a terminal
95
+ // (or idle-but-finished) state AND has no live session behind it is a
96
+ // lingering trace kept for the reap grace window. excludeTerminalTraces drops those
97
+ // rows so live-session reuse/spawn resolution can proceed; list/status
98
+ // keep excludeTerminalTraces=false so finished workers still appear.
99
+ if (excludeTerminalTraces
100
+ && isTerminalWorkerStatus(row.status || row.stage)
101
+ && !getLiveSession(sessionId)) {
102
+ return;
103
+ }
104
+ seen.add(sessionId);
105
+ rows.push({ tag, session: workerRowToSession(row), indexRow: row });
106
+ };
107
+ for (const row of readWorkerRows(context)) addIndexRow(row);
108
+ if (scanSessions) {
109
+ for (const session of mgr.listSessions({ includeClosed: false }) || []) {
110
+ const tag = agentTagOf(session);
111
+ add(session, tag);
112
+ if (tag) upsertWorkerSessionDeferred(session, tag);
113
+ }
114
+ }
115
+ for (const [tag, sessionId] of tags.entries()) {
116
+ add(getLiveSession(sessionId), tag);
117
+ }
118
+ return rows;
119
+ }
120
+
121
+ function nextTag(agent, context = {}) {
122
+ refreshTagsFromSessions({ context });
123
+ // Auto tags are agent + a per-agent local index with NO hyphen
124
+ // ("worker3", "heavy-worker7", or "agent1" when the agent is unset). The
125
+ // index is the max existing `^agent(\d+)$` + 1, escaping the agent so a
126
+ // hyphenated agent ("heavy-worker") is matched literally. Keep incrementing
127
+ // on any live collision.
128
+ const base = clean(agent) || 'agent';
129
+ const escaped = base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
130
+ const re = new RegExp(`^${escaped}(\\d+)$`);
131
+ let maxN = 0;
132
+ for (const existing of tags.keys()) {
133
+ const match = re.exec(existing);
134
+ if (!match) continue;
135
+ const n = Number(match[1]);
136
+ if (Number.isFinite(n) && n > maxN) maxN = n;
137
+ }
138
+ let n = maxN + 1;
139
+ let tag = `${base}${n}`;
140
+ while (resolveTag(tag, context)) tag = `${base}${++n}`;
141
+ return tag;
142
+ }
143
+
144
+ function refreshTagsFromSessions({ scanSessions = false, context = {} } = {}) {
145
+ transitionStaleNonterminalRows(context);
146
+ const indexedRows = refreshTagsFromIndex(context);
147
+ const indexedKeys = new Set(indexedRows.map((row) => `${row.tag}\0${row.sessionId}`));
148
+ for (const [tag, sessionId] of [...tags.entries()]) {
149
+ if (indexedKeys.has(`${tag}\0${sessionId}`)) continue;
150
+ const session = getLiveSession(sessionId);
151
+ if (!session || session.closed) {
152
+ tags.delete(tag);
153
+ tagAgents.delete(tag);
154
+ tagCwds.delete(tag);
155
+ }
156
+ }
157
+ if (!scanSessions) return;
158
+ for (const session of mgr.listSessions({ includeClosed: false }) || []) {
159
+ const tag = agentTagOf(session);
160
+ if (!tag || tags.has(tag)) continue;
161
+ if (!sessionMatchesContext(session, context)) continue;
162
+ tags.set(tag, session.id);
163
+ if (session.agent) tagAgents.set(tag, session.agent);
164
+ if (session.cwd) tagCwds.set(tag, session.cwd);
165
+ upsertWorkerSessionDeferred(session, tag);
166
+ }
167
+ }
168
+
169
+ function bindTag(tag, session, extra = {}) {
170
+ if (!tag || !session?.id) return;
171
+ tags.set(tag, session.id);
172
+ if (session.agent) tagAgents.set(tag, session.agent);
173
+ if (session.cwd) tagCwds.set(tag, session.cwd);
174
+ upsertWorkerSessionDeferred(session, tag, extra);
175
+ }
176
+
177
+ function forgetTag(tag) {
178
+ if (!tag) return;
179
+ const sessionId = tags.get(tag) || '';
180
+ tags.delete(tag);
181
+ tagAgents.delete(tag);
182
+ tagCwds.delete(tag);
183
+ removeWorkerRow({ tag, sessionId });
184
+ }
185
+
186
+ function forgetTerminalSession(tag, sessionId) {
187
+ const value = clean(tag);
188
+ const id = clean(sessionId);
189
+ if (value && id && tags.get(value) === id) {
190
+ tags.delete(value);
191
+ tagAgents.delete(value);
192
+ tagCwds.delete(value);
193
+ }
194
+ if (id) removeWorkerRow({ sessionId: id });
195
+ }
196
+
197
+ function tombstoneTerminalSession(tag, sessionId, session = null) {
198
+ const value = clean(tag);
199
+ const id = clean(sessionId);
200
+ if (!value || !id) {
201
+ forgetTerminalSession(value, id);
202
+ return;
203
+ }
204
+ if (tags.get(value) === id) {
205
+ tags.delete(value);
206
+ tagAgents.delete(value);
207
+ tagCwds.delete(value);
208
+ }
209
+ const tombstone = {
210
+ tag: value,
211
+ agent: clean(session?.agent) || null,
212
+ cwd: clean(session?.cwd) || null,
213
+ clientHostPid: positiveInt(session?.clientHostPid),
214
+ reapedAt: new Date().toISOString(),
215
+ };
216
+ flushWorkerIndexMutations();
217
+ writeWorkerRows((byKey, tombstonesByKey, priorityTombstoneKeys) => {
218
+ for (const [key, row] of [...byKey.entries()]) {
219
+ if (clean(row.sessionId) === id) byKey.delete(key);
220
+ }
221
+ const tombstoneKey = tagTombstoneKey(tombstone);
222
+ tombstonesByKey.set(tombstoneKey, tombstone);
223
+ priorityTombstoneKeys.add(tombstoneKey);
224
+ });
225
+ }
226
+
227
+ function tagTombstoneForTag(tag, context = {}) {
228
+ const value = clean(tag);
229
+ if (!value || value.startsWith('sess_')) return null;
230
+ return readTagTombstones(context).find((row) => row.tag === value) || null;
231
+ }
232
+
233
+ function consumeTagTombstone(tombstone) {
234
+ if (!tombstone?.tag) return false;
235
+ const key = tagTombstoneKey(tombstone);
236
+ flushWorkerIndexMutations();
237
+ writeWorkerRows((_byKey, tombstonesByKey) => tombstonesByKey.delete(key));
238
+ return true;
239
+ }
240
+
241
+ function cancelReap(sessionId) {
242
+ const handle = reapTimers.get(sessionId);
243
+ if (!handle) return false;
244
+ clearTimeout(handle);
245
+ reapTimers.delete(sessionId);
246
+ return true;
247
+ }
248
+
249
+ function scheduleReap(sessionId, provider = null) {
250
+ if (!sessionId) return;
251
+ cancelReap(sessionId);
252
+ const reapProvider = provider || getLiveSession(sessionId)?.provider || null;
253
+ const reapMs = resolveAgentTerminalReapMs(cfgMod.loadConfig(), reapProvider);
254
+ if (reapMs == null) return;
255
+ const handle = setTimeout(() => {
256
+ reapTimers.delete(sessionId);
257
+ const session = getLiveSession(sessionId);
258
+ try { mgr.hideSessionFromList?.(sessionId); } catch {}
259
+ const tag = tagForSession(sessionId);
260
+ tombstoneTerminalSession(tag, sessionId, session);
261
+ clearAgentStatuslineRoute(sessionId);
262
+ try { mgr.closeSession(sessionId, 'terminal-reap'); } catch {}
263
+ }, reapMs);
264
+ handle.unref?.();
265
+ reapTimers.set(sessionId, handle);
266
+ }
267
+
268
+ function transitionStaleNonterminalRows(context = {}) {
269
+ const staleRows = readWorkerRows(context).filter((row) => {
270
+ if (isTerminalWorkerStatus(row.status || row.stage)) return false;
271
+ if (getLiveSession(clean(row.sessionId))) return false;
272
+ const rowTime = workerRowTime(row);
273
+ const reapMs = resolveAgentTerminalReapMs(cfgMod.loadConfig(), row.provider);
274
+ // A row with no timestamp has no usable heartbeat at all. Explicitly
275
+ // disabled terminal reaping still gets the tombstone TTL as a finite
276
+ // stale-heartbeat bound, so malformed/running index rows cannot block a
277
+ // tag forever.
278
+ return rowTime <= 0 || Date.now() - rowTime >= (reapMs ?? TAG_TOMBSTONE_TTL_MS);
279
+ });
280
+ if (staleRows.length === 0) return false;
281
+ flushWorkerIndexMutations();
282
+ const nowIso = new Date().toISOString();
283
+ writeWorkerRows((byKey, tombstonesByKey, priorityTombstoneKeys) => {
284
+ for (const row of staleRows) {
285
+ const sessionId = clean(row.sessionId);
286
+ for (const [key, candidate] of [...byKey.entries()]) {
287
+ if (clean(candidate.sessionId) === sessionId) byKey.delete(key);
288
+ }
289
+ const tombstone = {
290
+ tag: clean(row.tag),
291
+ agent: clean(row.agent) || null,
292
+ cwd: clean(row.cwd) || null,
293
+ clientHostPid: positiveInt(row.clientHostPid),
294
+ reapedAt: nowIso,
295
+ };
296
+ const tombstoneKey = tagTombstoneKey(tombstone);
297
+ tombstonesByKey.set(tombstoneKey, tombstone);
298
+ priorityTombstoneKeys.add(tombstoneKey);
299
+ if (tags.get(tombstone.tag) === sessionId) {
300
+ tags.delete(tombstone.tag);
301
+ tagAgents.delete(tombstone.tag);
302
+ tagCwds.delete(tombstone.tag);
303
+ }
304
+ }
305
+ });
306
+ return true;
307
+ }
308
+
309
+ return {
310
+ readAllTagTombstones,
311
+ readTagTombstones,
312
+ readWorkerRows,
313
+ writeWorkerRows,
314
+ flushWorkerIndexMutations,
315
+ upsertWorkerSession,
316
+ upsertWorkerSessionDeferred,
317
+ removeWorkerRow,
318
+ refreshTagsFromIndex,
319
+ tags,
320
+ tagAgents,
321
+ tagCwds,
322
+ reapTimers,
323
+ wantsSessionScan,
324
+ resolveTag,
325
+ getLiveSession,
326
+ tagForSession,
327
+ agentSessionEntries,
328
+ nextTag,
329
+ refreshTagsFromSessions,
330
+ bindTag,
331
+ forgetTag,
332
+ forgetTerminalSession,
333
+ tombstoneTerminalSession,
334
+ tagTombstoneForTag,
335
+ consumeTagTombstone,
336
+ cancelReap,
337
+ scheduleReap,
338
+ transitionStaleNonterminalRows,
339
+ };
340
+ }