mixdog 1.3.0 → 1.3.1
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/NOTICE.md +52 -9
- package/package.json +1 -1
- package/src/defaults/shipped-history.json +4 -1
- package/src/defaults/skills/setup/references/actions.md +4 -1
- package/src/defaults/skills/setup-guide/SKILL.md +10 -7
- package/src/output-styles/common.md +13 -8
- package/src/rules/agent-mode/AGENT_MODE.md +1 -1
- package/src/rules/agent-mode/MIXY.md +11 -4
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +72 -14
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -2
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +14 -4
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +37 -7
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +4 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +17 -8
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +32 -5
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +8 -14
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -20
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +17 -1
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +61 -9
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +14 -9
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
- package/src/runtime/attachments/store.mjs +6 -0
- package/src/runtime/effort-judge/judge-client.mjs +1 -1
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/shared/agent-kind.mjs +152 -0
- package/src/runtime/shared/agent-route-config.d.mts +1 -0
- package/src/runtime/shared/agent-route-config.mjs +22 -8
- package/src/runtime/shared/agent-store.mjs +132 -0
- package/src/runtime/shared/bot-split-migration.mjs +457 -0
- package/src/runtime/shared/config-patch.mjs +20 -1
- package/src/runtime/shared/connector-catalog.d.mts +1 -2
- package/src/runtime/shared/connector-catalog.mjs +2 -4
- package/src/runtime/shared/connector-triggers.mjs +2 -0
- package/src/runtime/shared/err-text.mjs +3 -1
- package/src/runtime/shared/error-presentation.mjs +91 -6
- package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
- package/src/runtime/shared/handoff-artifacts.mjs +61 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
- package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
- package/src/runtime/shared/model-route-fallback.mjs +79 -7
- package/src/runtime/shared/plain-preview.mjs +22 -17
- package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
- package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
- package/src/runtime/shared/usage-readout-totals.mjs +15 -13
- package/src/runtime/shared/user-data-guard.mjs +5 -2
- package/src/session-runtime/agent-home-delivery.mjs +423 -56
- package/src/session-runtime/agent-room-delivery.mjs +45 -4
- package/src/session-runtime/agent-task-sessions.mjs +299 -39
- package/src/session-runtime/boot/apis.mjs +13 -0
- package/src/session-runtime/boot/providers.mjs +14 -4
- package/src/session-runtime/config-lifecycle.mjs +43 -2
- package/src/session-runtime/handoff-attachments.mjs +593 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +27 -3
- package/src/session-runtime/model-route/route-persist.mjs +2 -2
- package/src/session-runtime/model-route/route-tuning.mjs +7 -1
- package/src/session-runtime/model-route/set-route.mjs +21 -1
- package/src/session-runtime/native-web-search.mjs +2 -2
- package/src/session-runtime/new-session-config.mjs +6 -1
- package/src/session-runtime/route-resolution.mjs +5 -11
- package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
- package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
- package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
- package/src/session-runtime/setup-tool/executor.mjs +12 -1
- package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
- package/src/session-runtime/setup-tool/settings-contract.mjs +6 -0
- package/src/session-runtime/tool-policy-refresh.mjs +13 -2
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +11 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +123 -32
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
- package/src/standalone/daemon-session-control.mjs +13 -5
- package/src/standalone/daemon.mjs +16 -4
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
- package/src/standalone/session-service/agent-tree.mjs +4 -3
- package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
- package/src/tui/dist/index.mjs +70 -9
- package/src/tui/session/notice-surface.mjs +3 -4
- package/src/tui/session/session-api/routes.mjs +14 -3
- package/src/tui/session/transcript-intake.mjs +2 -0
- package/src/tui/session/turn-live-call.mjs +11 -3
- package/src/tui/session/turn.mjs +13 -3
|
@@ -18,14 +18,22 @@
|
|
|
18
18
|
* registers the live surface so a thread open in a window shows the notice at
|
|
19
19
|
* once; a cold thread picks it up from its stored record.
|
|
20
20
|
*/
|
|
21
|
+
import { randomUUID } from 'node:crypto';
|
|
21
22
|
import { readFile } from 'node:fs/promises';
|
|
22
23
|
import { join } from 'node:path';
|
|
23
24
|
import {
|
|
24
25
|
MIXY_AGENT_ID,
|
|
25
26
|
agentHomeSessionId,
|
|
26
27
|
agentIdOfHomeSession,
|
|
28
|
+
isAgentHomeSessionId,
|
|
27
29
|
} from '../runtime/agent/orchestrator/session/agent-home.mjs';
|
|
28
30
|
import {
|
|
31
|
+
isRoomThreadSessionId,
|
|
32
|
+
roomIdOfSession,
|
|
33
|
+
roomMemberOfSession,
|
|
34
|
+
} from '../runtime/agent/orchestrator/session/agent-room.mjs';
|
|
35
|
+
import {
|
|
36
|
+
TASK_LOOP_CAP,
|
|
29
37
|
agentTaskMessage,
|
|
30
38
|
agentTaskWakePrompt,
|
|
31
39
|
routineOmittedMessage,
|
|
@@ -36,6 +44,7 @@ import {
|
|
|
36
44
|
} from '../runtime/agent/orchestrator/session/routine-result.mjs';
|
|
37
45
|
import { parseJsonText, updateJsonAtomic } from '../runtime/shared/atomic-file.mjs';
|
|
38
46
|
import { ROUTINE_LIMITS } from '../runtime/shared/routine-limits.mjs';
|
|
47
|
+
import { submissionIdsOf } from './agent-task-sessions.mjs';
|
|
39
48
|
import {
|
|
40
49
|
heldRequestMessage,
|
|
41
50
|
heldRequestNotice,
|
|
@@ -49,9 +58,14 @@ function deliveredRow(item, messageId) {
|
|
|
49
58
|
return { message, notice: heldRequestNotice(message) };
|
|
50
59
|
}
|
|
51
60
|
if (item.kind === 'agent-task') {
|
|
52
|
-
// A turn's end (not its start or a wait for the user) wakes the agent
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
// A turn's end (not its start or a wait for the user) wakes the agent —
|
|
62
|
+
// except past the round-trip cap with another agent's own thread, where
|
|
63
|
+
// each wake could answer the other agent again without end.
|
|
64
|
+
const capped =
|
|
65
|
+
(isAgentHomeSessionId(item.taskSessionId) || isRoomThreadSessionId(item.taskSessionId)) &&
|
|
66
|
+
(item.round || 0) > TASK_LOOP_CAP;
|
|
67
|
+
const message = agentTaskMessage({ ...item, messageId, ...(capped ? { woken: false } : {}) });
|
|
68
|
+
return { message, notice: routineResultNotice(message), wake: item.roundTrip === true && !capped };
|
|
55
69
|
}
|
|
56
70
|
const message = routineResultMessage({ ...item, messageId });
|
|
57
71
|
return { message, notice: routineResultNotice(message) };
|
|
@@ -64,9 +78,10 @@ export function scheduleOwnerAgentId(schedule) {
|
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
let liveSurface = null;
|
|
67
|
-
/** `{ busy(sessionId), notice(sessionId, notice), wake(sessionId, prompt),
|
|
81
|
+
/** `{ busy(sessionId), notice(sessionId, notice), wake(sessionId, prompt, { id }),
|
|
68
82
|
* submit(sessionId, prompt, options), abort(sessionId) }` from the session
|
|
69
|
-
* service; `wake` starts one hidden turn
|
|
83
|
+
* service; `wake` starts one hidden turn with submission `id` (resolves
|
|
84
|
+
* false when it was refused), `submit` queues a routine turn
|
|
70
85
|
* (resolves whether it was accepted), `abort` stops the running turn. */
|
|
71
86
|
export function setAgentHomeLiveSurface(surface) {
|
|
72
87
|
liveSurface = surface || null;
|
|
@@ -80,19 +95,72 @@ export function agentHomeLiveSurface() {
|
|
|
80
95
|
// Run ids already posted, kept per home so a replayed run never posts twice,
|
|
81
96
|
// even into a thread that was reset ("대화 새로 시작") since.
|
|
82
97
|
const DELIVERED_LEDGER = 500;
|
|
98
|
+
// A target's round-trip count left idle this long starts over.
|
|
99
|
+
export const ROUND_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
100
|
+
// Per home: round-trip targets counted (least recently counted drop first),
|
|
101
|
+
// reporting turns watched at once, and posted results owed a wake.
|
|
102
|
+
export const QUEUE_CAPS = Object.freeze({ roundTargets: 100, watches: 200, wakeRunIds: 100 });
|
|
103
|
+
|
|
104
|
+
/** `homeId`'s wake obligations in `wakes`: posted task results not in a batch
|
|
105
|
+
* yet (`owed`), those past the cap collapsed into one marker (`overflow`:
|
|
106
|
+
* `{ count, oldest, newest }` run ids) the next batch takes over, and
|
|
107
|
+
* batches (`{ id, runIds, overflow? }`) stored before their wake turn was
|
|
108
|
+
* submitted under that id. */
|
|
109
|
+
function wakeState(wakes, homeId) {
|
|
110
|
+
const raw = wakes[homeId];
|
|
111
|
+
if (Array.isArray(raw)) return { owed: raw, batches: [], overflow: null };
|
|
112
|
+
return {
|
|
113
|
+
owed: Array.isArray(raw?.owed) ? raw.owed : [],
|
|
114
|
+
batches: Array.isArray(raw?.batches)
|
|
115
|
+
? raw.batches.filter((batch) => typeof batch?.id === 'string' && Array.isArray(batch.runIds))
|
|
116
|
+
: [],
|
|
117
|
+
overflow: raw?.overflow?.count > 0 ? raw.overflow : null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const owesWake = (state) => state.owed.length > 0 || state.batches.length > 0 || Boolean(state.overflow);
|
|
122
|
+
|
|
123
|
+
function withWakeState(doc, homeId, { owed, batches, overflow }) {
|
|
124
|
+
const wakes = { ...doc.wakes };
|
|
125
|
+
if (owesWake({ owed, batches, overflow })) wakes[homeId] = { owed, batches, ...(overflow ? { overflow } : {}) };
|
|
126
|
+
else delete wakes[homeId];
|
|
127
|
+
return { ...doc, wakes };
|
|
128
|
+
}
|
|
83
129
|
|
|
84
130
|
/** The persisted queue at `filePath`: `{ homes: { [homeSessionId]: item[] },
|
|
85
131
|
* omitted: { [homeSessionId]: { id, count } }, delivered: { [homeSessionId]:
|
|
86
132
|
* runId[] } }`. A home keeps at most `limit` items: past it the oldest drop,
|
|
87
133
|
* counted under `omitted` so the thread gets one "n omitted" notice. */
|
|
88
|
-
export function createRoutineResultQueue(
|
|
134
|
+
export function createRoutineResultQueue(
|
|
135
|
+
filePath,
|
|
136
|
+
{ limit = ROUTINE_LIMITS.homeQueueLimit, now = Date.now, caps = QUEUE_CAPS } = {}
|
|
137
|
+
) {
|
|
89
138
|
const section = (doc, key) => (doc && typeof doc[key] === 'object' && doc[key]) || {};
|
|
139
|
+
/** `own`'s per-target counts (`{ n, at }`) used within ROUND_TTL_MS, the
|
|
140
|
+
* `caps.roundTargets` most recently counted. */
|
|
141
|
+
const freshRounds = (own) =>
|
|
142
|
+
Object.fromEntries(
|
|
143
|
+
Object.entries(own && typeof own === 'object' ? own : {})
|
|
144
|
+
.filter(([, count]) => count && typeof count === 'object' && now() - (Number(count.at) || 0) < ROUND_TTL_MS)
|
|
145
|
+
.sort(([, a], [, b]) => (Number(b.at) || 0) - (Number(a.at) || 0))
|
|
146
|
+
.slice(0, caps.roundTargets)
|
|
147
|
+
);
|
|
148
|
+
const tidyRounds = (rounds) =>
|
|
149
|
+
Object.fromEntries(
|
|
150
|
+
Object.entries(rounds || {})
|
|
151
|
+
.map(([homeId, own]) => [homeId, freshRounds(own)])
|
|
152
|
+
.filter(([, own]) => Object.keys(own).length)
|
|
153
|
+
);
|
|
90
154
|
const parts = (doc) => ({
|
|
91
155
|
homes: section(doc, 'homes'),
|
|
92
156
|
omitted: section(doc, 'omitted'),
|
|
93
157
|
delivered: section(doc, 'delivered'),
|
|
94
158
|
// Agent-initiated round trips per task session (session create).
|
|
95
159
|
rounds: section(doc, 'rounds'),
|
|
160
|
+
// Reporting turns not yet queued, by submission id (agent-task-sessions.mjs).
|
|
161
|
+
watches: section(doc, 'watches'),
|
|
162
|
+
// Posted task results whose wake turn was not taken yet, per home.
|
|
163
|
+
wakes: section(doc, 'wakes'),
|
|
96
164
|
});
|
|
97
165
|
async function read() {
|
|
98
166
|
try {
|
|
@@ -102,6 +170,15 @@ export function createRoutineResultQueue(filePath, { limit = ROUTINE_LIMITS.home
|
|
|
102
170
|
throw error;
|
|
103
171
|
}
|
|
104
172
|
}
|
|
173
|
+
/** Read-modify-write under the file lock (`mutator(doc)` returns the next
|
|
174
|
+
* doc, or nothing to keep the file); every write also retires stale and
|
|
175
|
+
* surplus round counts. */
|
|
176
|
+
function update(mutator) {
|
|
177
|
+
return updateJsonAtomic(filePath, (raw) => {
|
|
178
|
+
const next = mutator(parts(raw));
|
|
179
|
+
return next ? { ...next, rounds: tidyRounds(next.rounds) } : undefined;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
105
182
|
/** `doc` with `items` appended to `homeId`'s list, minus run ids it holds or posted. */
|
|
106
183
|
function appended(doc, homeId, items, omittedCount = 0) {
|
|
107
184
|
const list = doc.homes[homeId] || [];
|
|
@@ -120,8 +197,80 @@ export function createRoutineResultQueue(filePath, { limit = ROUTINE_LIMITS.home
|
|
|
120
197
|
}
|
|
121
198
|
return {
|
|
122
199
|
async pendingHomes() {
|
|
123
|
-
const { homes, omitted } = await read();
|
|
124
|
-
return [
|
|
200
|
+
const { homes, omitted, wakes } = await read();
|
|
201
|
+
return [
|
|
202
|
+
...new Set([
|
|
203
|
+
...Object.keys(homes).filter((id) => homes[id]?.length),
|
|
204
|
+
...Object.keys(omitted),
|
|
205
|
+
...Object.keys(wakes).filter((id) => owesWake(wakeState(wakes, id))),
|
|
206
|
+
]),
|
|
207
|
+
];
|
|
208
|
+
},
|
|
209
|
+
/** `homeId`'s wake obligations: `{ owed, batches, overflow }` (wakeState). */
|
|
210
|
+
async wakes(homeId) {
|
|
211
|
+
return wakeState((await read()).wakes, homeId);
|
|
212
|
+
},
|
|
213
|
+
/** Store the owed results (and overflow marker) of `homeId` as one new
|
|
214
|
+
* batch under a fresh wake id, before its wake is submitted; null when
|
|
215
|
+
* none are owed. */
|
|
216
|
+
async batchWake(homeId) {
|
|
217
|
+
let batch = null;
|
|
218
|
+
await update((doc) => {
|
|
219
|
+
batch = null;
|
|
220
|
+
const state = wakeState(doc.wakes, homeId);
|
|
221
|
+
if (!state.owed.length && !state.overflow) return undefined;
|
|
222
|
+
batch = {
|
|
223
|
+
id: `awake_${randomUUID()}`,
|
|
224
|
+
runIds: state.owed,
|
|
225
|
+
...(state.overflow ? { overflow: state.overflow } : {}),
|
|
226
|
+
};
|
|
227
|
+
return withWakeState(doc, homeId, { owed: [], batches: [...state.batches, batch], overflow: null });
|
|
228
|
+
});
|
|
229
|
+
return batch;
|
|
230
|
+
},
|
|
231
|
+
/** The wake of batch `batchId` was taken (or is moot): it is owed no more. */
|
|
232
|
+
async woke(homeId, batchId) {
|
|
233
|
+
await update((doc) => {
|
|
234
|
+
const state = wakeState(doc.wakes, homeId);
|
|
235
|
+
const batches = state.batches.filter((batch) => batch.id !== batchId);
|
|
236
|
+
if (batches.length === state.batches.length) return undefined;
|
|
237
|
+
return withWakeState(doc, homeId, { ...state, batches });
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
/** Drop every wake obligation of `homeId` (its owner is gone). */
|
|
241
|
+
async dropWakes(homeId) {
|
|
242
|
+
await update((doc) =>
|
|
243
|
+
doc.wakes[homeId] ? withWakeState(doc, homeId, { owed: [], batches: [], overflow: null }) : undefined
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
/** Every home the rounds, wakes, posted ledger or watches name. */
|
|
247
|
+
async bookkeptHomes() {
|
|
248
|
+
const doc = await read();
|
|
249
|
+
const watched = Object.values(doc.watches).map((watch) => watch?.homeSessionId);
|
|
250
|
+
return [
|
|
251
|
+
...new Set(
|
|
252
|
+
[...Object.keys(doc.rounds), ...Object.keys(doc.wakes), ...Object.keys(doc.delivered), ...watched].filter(
|
|
253
|
+
(id) => typeof id === 'string' && id
|
|
254
|
+
)
|
|
255
|
+
),
|
|
256
|
+
];
|
|
257
|
+
},
|
|
258
|
+
/** Retire the rounds, wakes, posted ledger and watches of the `gone`
|
|
259
|
+
* homes (and, as every write does, stale round counts). */
|
|
260
|
+
async prune(gone) {
|
|
261
|
+
await update((doc) => {
|
|
262
|
+
const kept = (part) => Object.fromEntries(Object.entries(part).filter(([homeId]) => !gone.includes(homeId)));
|
|
263
|
+
const next = {
|
|
264
|
+
...doc,
|
|
265
|
+
rounds: tidyRounds(kept(doc.rounds)),
|
|
266
|
+
wakes: kept(doc.wakes),
|
|
267
|
+
delivered: kept(doc.delivered),
|
|
268
|
+
watches: Object.fromEntries(
|
|
269
|
+
Object.entries(doc.watches).filter(([, watch]) => !gone.includes(watch?.homeSessionId))
|
|
270
|
+
),
|
|
271
|
+
};
|
|
272
|
+
return JSON.stringify(next) === JSON.stringify(doc) ? undefined : next;
|
|
273
|
+
});
|
|
125
274
|
},
|
|
126
275
|
async items(homeId) {
|
|
127
276
|
return (await read()).homes[homeId] || [];
|
|
@@ -130,20 +279,72 @@ export function createRoutineResultQueue(filePath, { limit = ROUTINE_LIMITS.home
|
|
|
130
279
|
async omitted(homeId) {
|
|
131
280
|
return (await read()).omitted[homeId] || null;
|
|
132
281
|
},
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
282
|
+
/** Queue `item` for `homeId`; `unwatch` drops that watch in the same write,
|
|
283
|
+
* also when the item was queued or posted before. */
|
|
284
|
+
async add(homeId, item, { unwatch = '' } = {}) {
|
|
285
|
+
await update((doc) => {
|
|
286
|
+
let next = null;
|
|
287
|
+
if (item.roundTrip !== true) next = appended(doc, homeId, [item]);
|
|
288
|
+
else {
|
|
289
|
+
// A task turn's end counts one round trip, once per run id, per home
|
|
290
|
+
// and target: the user's next message in that home starts over
|
|
291
|
+
// (resetRounds), and so does a count idle past ROUND_TTL_MS.
|
|
292
|
+
const own = freshRounds(doc.rounds[homeId]);
|
|
293
|
+
const round = (own[item.taskSessionId]?.n || 0) + 1;
|
|
294
|
+
next = appended(doc, homeId, [{ ...item, round }]);
|
|
295
|
+
if (next) {
|
|
296
|
+
const counted = { ...own, [item.taskSessionId]: { n: round, at: now() } };
|
|
297
|
+
next = { ...next, rounds: { ...doc.rounds, [homeId]: counted } };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (!unwatch || !doc.watches[unwatch]) return next ?? undefined;
|
|
301
|
+
const watches = { ...doc.watches };
|
|
302
|
+
delete watches[unwatch];
|
|
303
|
+
return { ...(next || doc), watches };
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
/** Keep `watch` ({ runId, agentId, homeSessionId, taskSessionId, title,
|
|
307
|
+
* target? }) until its report is queued, dropping the watches `drop` in
|
|
308
|
+
* the same write. Throws when its home already keeps `caps.watches`. */
|
|
309
|
+
async watch(watch, { drop = [] } = {}) {
|
|
310
|
+
await update((doc) => {
|
|
311
|
+
const watches = { ...doc.watches };
|
|
312
|
+
for (const runId of drop) delete watches[runId];
|
|
313
|
+
const own = Object.values(watches).filter(
|
|
314
|
+
(kept) => kept?.homeSessionId === watch.homeSessionId && kept.runId !== watch.runId
|
|
315
|
+
);
|
|
316
|
+
if (own.length >= caps.watches) {
|
|
317
|
+
throw new Error(`${watch.homeSessionId} already waits on ${own.length} session reports`);
|
|
318
|
+
}
|
|
319
|
+
return { ...doc, watches: { ...watches, [watch.runId]: watch } };
|
|
320
|
+
});
|
|
321
|
+
},
|
|
322
|
+
watchCap: caps.watches,
|
|
323
|
+
/** Every stored watch, oldest first. */
|
|
324
|
+
async watches() {
|
|
325
|
+
return Object.values((await read()).watches);
|
|
326
|
+
},
|
|
327
|
+
/** Drop the watch of `runId` (its turn was never taken). */
|
|
328
|
+
async unwatch(runId) {
|
|
329
|
+
await update((doc) => {
|
|
330
|
+
if (!doc.watches[runId]) return undefined;
|
|
331
|
+
const watches = { ...doc.watches };
|
|
332
|
+
delete watches[runId];
|
|
333
|
+
return { ...doc, watches };
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
/** The user spoke in `homeId`: its round trips count from zero again. */
|
|
337
|
+
async resetRounds(homeId) {
|
|
338
|
+
await update((doc) => {
|
|
339
|
+
if (!doc.rounds[homeId]) return undefined;
|
|
340
|
+
const rounds = { ...doc.rounds };
|
|
341
|
+
delete rounds[homeId];
|
|
342
|
+
return { ...doc, rounds };
|
|
141
343
|
});
|
|
142
344
|
},
|
|
143
345
|
/** Hand every queued item (and omission) of `fromHome` to `toHome`. */
|
|
144
346
|
async move(fromHome, toHome) {
|
|
145
|
-
await
|
|
146
|
-
const doc = parts(raw);
|
|
347
|
+
await update((doc) => {
|
|
147
348
|
const items = doc.homes[fromHome] || [];
|
|
148
349
|
const dropped = doc.omitted[fromHome]?.count || 0;
|
|
149
350
|
const next = appended(doc, toHome, items, dropped) || doc;
|
|
@@ -154,10 +355,38 @@ export function createRoutineResultQueue(filePath, { limit = ROUTINE_LIMITS.home
|
|
|
154
355
|
return { ...next, homes, omitted };
|
|
155
356
|
});
|
|
156
357
|
},
|
|
157
|
-
/** Dequeue the posted `runIds` (and the posted omission notice `omitted`)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
358
|
+
/** Dequeue the posted `runIds` (and the posted omission notice `omitted`);
|
|
359
|
+
* those in `wake` stay owed a wake (`wakes`) in the same write, at most
|
|
360
|
+
* `caps.wakeRunIds` run ids per home: past it the oldest owed collapse
|
|
361
|
+
* into the overflow marker the next batch takes over (`overflowed`
|
|
362
|
+
* counts them). Stored batches never change. */
|
|
363
|
+
async remove(homeId, runIds, omitted = null, { wake = [] } = {}) {
|
|
364
|
+
let overflowed = 0;
|
|
365
|
+
await update((doc) => {
|
|
366
|
+
overflowed = 0;
|
|
367
|
+
const state = wakeState(doc.wakes, homeId);
|
|
368
|
+
const batched = state.batches.flatMap((batch) => batch.runIds);
|
|
369
|
+
const known = new Set([
|
|
370
|
+
...state.owed,
|
|
371
|
+
...batched,
|
|
372
|
+
...[state.overflow?.oldest, state.overflow?.newest].filter(Boolean),
|
|
373
|
+
]);
|
|
374
|
+
const added = wake.filter((runId) => !known.has(runId));
|
|
375
|
+
let wakes = doc.wakes;
|
|
376
|
+
if (added.length) {
|
|
377
|
+
const owed = [...state.owed, ...added];
|
|
378
|
+
const room = Math.max(0, caps.wakeRunIds - batched.length);
|
|
379
|
+
const excess = owed.slice(0, Math.max(0, owed.length - room));
|
|
380
|
+
overflowed = excess.length;
|
|
381
|
+
const overflow = excess.length
|
|
382
|
+
? {
|
|
383
|
+
count: (state.overflow?.count || 0) + excess.length,
|
|
384
|
+
oldest: state.overflow?.oldest || excess[0],
|
|
385
|
+
newest: excess.at(-1),
|
|
386
|
+
}
|
|
387
|
+
: state.overflow;
|
|
388
|
+
({ wakes } = withWakeState(doc, homeId, { ...state, owed: owed.slice(excess.length), overflow }));
|
|
389
|
+
}
|
|
161
390
|
const homes = { ...doc.homes };
|
|
162
391
|
const rest = (homes[homeId] || []).filter((row) => !runIds.includes(row.runId));
|
|
163
392
|
if (rest.length) homes[homeId] = rest;
|
|
@@ -172,8 +401,9 @@ export function createRoutineResultQueue(filePath, { limit = ROUTINE_LIMITS.home
|
|
|
172
401
|
if (left > 0) omittedNow[homeId] = { id: `${current.id}.${omitted.count}`, count: left };
|
|
173
402
|
else delete omittedNow[homeId];
|
|
174
403
|
}
|
|
175
|
-
return { ...doc, homes, omitted: omittedNow, delivered };
|
|
404
|
+
return { ...doc, homes, omitted: omittedNow, delivered, wakes };
|
|
176
405
|
});
|
|
406
|
+
return { overflowed };
|
|
177
407
|
},
|
|
178
408
|
};
|
|
179
409
|
}
|
|
@@ -191,6 +421,12 @@ export function createAgentHomeDelivery({
|
|
|
191
421
|
missingRetryMs = 60_000,
|
|
192
422
|
// 'present' | 'deleted' | 'unknown' per agent id: a deleted owner's results go to Mixy.
|
|
193
423
|
agentPresence = () => 'unknown',
|
|
424
|
+
// The same per room id. The daemon has no room store (rooms live in the
|
|
425
|
+
// desktop host), so by default no room counts as deleted.
|
|
426
|
+
roomPresence = () => 'unknown',
|
|
427
|
+
// True only on positive evidence that session `id` was deleted; an
|
|
428
|
+
// unreadable or unknown record is not deleted.
|
|
429
|
+
sessionDeleted = () => false,
|
|
194
430
|
log = (line) => process.stderr.write(`mixdog routines: ${line}\n`),
|
|
195
431
|
}) {
|
|
196
432
|
const busy = (sessionId) => {
|
|
@@ -236,13 +472,15 @@ export function createAgentHomeDelivery({
|
|
|
236
472
|
async function drainOnce(homeId) {
|
|
237
473
|
const items = await queue.items(homeId);
|
|
238
474
|
const omitted = await queue.omitted(homeId);
|
|
239
|
-
|
|
475
|
+
const owed = owesWake(await queue.wakes(homeId));
|
|
476
|
+
if (!items.length && !omitted && !owed) return;
|
|
240
477
|
const agentId = agentIdOfHomeSession(homeId);
|
|
241
478
|
if (agentId && agentId !== MIXY_AGENT_ID && agentPresence(agentId) === 'deleted') {
|
|
242
479
|
// The owner is gone (its home archived): Mixy, the default owner, gets them.
|
|
243
480
|
const mixyHome = agentHomeSessionId(MIXY_AGENT_ID);
|
|
244
481
|
log(`${items.length} result(s) of the deleted agent ${agentId} go to ${mixyHome}`);
|
|
245
482
|
await queue.move(homeId, mixyHome);
|
|
483
|
+
if (owed) await queue.dropWakes(homeId);
|
|
246
484
|
void drain(mixyHome);
|
|
247
485
|
return;
|
|
248
486
|
}
|
|
@@ -265,11 +503,11 @@ export function createAgentHomeDelivery({
|
|
|
265
503
|
session.messages.push(message);
|
|
266
504
|
fresh.push(message);
|
|
267
505
|
}
|
|
268
|
-
const
|
|
506
|
+
const toWake = [];
|
|
269
507
|
for (const item of items) {
|
|
270
508
|
const row = deliveredRow(item, messageIdOf(item.runId));
|
|
271
|
-
// Every dequeued task result
|
|
272
|
-
if (row.wake)
|
|
509
|
+
// Every dequeued task result is owed one wake, posted in this pass or before.
|
|
510
|
+
if (row.wake) toWake.push(item.runId);
|
|
273
511
|
if (posted.has(messageIdOf(item.runId))) continue;
|
|
274
512
|
session.messages.push(row.message);
|
|
275
513
|
fresh.push(row);
|
|
@@ -286,20 +524,48 @@ export function createAgentHomeDelivery({
|
|
|
286
524
|
}
|
|
287
525
|
}
|
|
288
526
|
}
|
|
289
|
-
|
|
527
|
+
// Dequeuing and owing the wake are one write: no restart between posting
|
|
528
|
+
// and waking loses the wake.
|
|
529
|
+
const { overflowed } = await queue.remove(
|
|
290
530
|
homeId,
|
|
291
531
|
items.map((item) => item.runId),
|
|
292
|
-
omitted
|
|
532
|
+
omitted,
|
|
533
|
+
{ wake: toWake }
|
|
293
534
|
);
|
|
294
|
-
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
535
|
+
if (overflowed) log(`${homeId} is at its wake cap: ${overflowed} result(s) ride the next wake as a count`);
|
|
536
|
+
// Owed task results wake the agent with one hidden turn per batch. A batch
|
|
537
|
+
// is stored with its wake id BEFORE the wake is submitted and dropped once
|
|
538
|
+
// it is taken, so a restart in between re-checks that exact id; results
|
|
539
|
+
// owed later form a new batch. A replayed run id is never queued again.
|
|
540
|
+
const byId = new Map(session.messages.map((message) => [message?.meta?.transcript?.messageId, message]));
|
|
541
|
+
const taken = new Set(session.messages.flatMap((message) => submissionIdsOf(message)));
|
|
542
|
+
let batch = null;
|
|
543
|
+
let woken = [];
|
|
544
|
+
let more = 0;
|
|
545
|
+
for (;;) {
|
|
546
|
+
batch = (await queue.wakes(homeId)).batches[0] || (await queue.batchWake(homeId));
|
|
547
|
+
if (!batch) return;
|
|
548
|
+
const stale = taken.has(batch.id);
|
|
549
|
+
woken = stale ? [] : batch.runIds.map((runId) => byId.get(messageIdOf(runId))).filter(Boolean);
|
|
550
|
+
// Results past the wake cap ride this wake as a count of rows to read.
|
|
551
|
+
more = !stale && byId.has(messageIdOf(batch.overflow?.newest)) ? batch.overflow.count : 0;
|
|
552
|
+
if (woken.length || more) break;
|
|
553
|
+
// Taken before a restart, or its rows gone with a thread reset.
|
|
554
|
+
await queue.woke(homeId, batch.id);
|
|
555
|
+
}
|
|
556
|
+
const surface = live();
|
|
557
|
+
if (typeof surface?.wake !== 'function') return later(homeId, busyRetryMs);
|
|
558
|
+
try {
|
|
559
|
+
if ((await surface.wake(homeId, agentTaskWakePrompt(woken, { more }), { id: batch.id })) === false) {
|
|
560
|
+
throw new Error('the wake turn was refused');
|
|
301
561
|
}
|
|
562
|
+
} catch (error) {
|
|
563
|
+
log(`waking ${homeId} failed, retrying: ${error?.message || error}`);
|
|
564
|
+
return later(homeId, missingRetryMs);
|
|
302
565
|
}
|
|
566
|
+
await queue.woke(homeId, batch.id);
|
|
567
|
+
// Another batch left by an earlier process wakes once this turn is done.
|
|
568
|
+
if (owesWake(await queue.wakes(homeId))) later(homeId, busyRetryMs);
|
|
303
569
|
}
|
|
304
570
|
|
|
305
571
|
/** The single drainer of `homeId`; a call during a pass runs one more pass. */
|
|
@@ -365,19 +631,30 @@ export function createAgentHomeDelivery({
|
|
|
365
631
|
/** Persist one report of a coding task session `agentId` created (session
|
|
366
632
|
* create) for its home thread, then start draining it. `status` 'started'
|
|
367
633
|
* and 'blocked' only post a line; a turn's end also wakes the agent. */
|
|
368
|
-
async function enqueueTask(
|
|
634
|
+
async function enqueueTask(
|
|
635
|
+
agentId,
|
|
636
|
+
{ runId, taskSessionId, title, status, text = '', target, watchId = '', files = [], omitted = 0 }
|
|
637
|
+
) {
|
|
369
638
|
const homeId = agentHomeSessionId(agentId) || agentHomeSessionId(MIXY_AGENT_ID);
|
|
370
639
|
if (!runId || !taskSessionId) throw new Error('task report: runId and taskSessionId required');
|
|
371
|
-
await queue.add(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
640
|
+
await queue.add(
|
|
641
|
+
homeId,
|
|
642
|
+
{
|
|
643
|
+
runId: String(runId),
|
|
644
|
+
kind: 'agent-task',
|
|
645
|
+
taskSessionId,
|
|
646
|
+
title,
|
|
647
|
+
status,
|
|
648
|
+
text: routineSummary(text),
|
|
649
|
+
// Any session other than a task session the agent created.
|
|
650
|
+
...(target === 'session' ? { target } : {}),
|
|
651
|
+
// The files the turn produced (handoff-attachments.mjs collectTurnArtifacts).
|
|
652
|
+
...(Array.isArray(files) && files.length ? { files, ...(omitted > 0 ? { omitted } : {}) } : {}),
|
|
653
|
+
roundTrip: status !== 'started' && status !== 'blocked',
|
|
654
|
+
at: Date.now(),
|
|
655
|
+
},
|
|
656
|
+
{ unwatch: watchId ? String(watchId) : '' }
|
|
657
|
+
);
|
|
381
658
|
void drain(homeId);
|
|
382
659
|
return homeId;
|
|
383
660
|
}
|
|
@@ -398,27 +675,87 @@ export function createAgentHomeDelivery({
|
|
|
398
675
|
}
|
|
399
676
|
}
|
|
400
677
|
|
|
401
|
-
/**
|
|
678
|
+
/** A bookkept home proven deleted: a bot's home whose definition is gone,
|
|
679
|
+
* or a room thread whose room is. Never Mixy's home, never on a missing
|
|
680
|
+
* thread record, never on an unknown answer. */
|
|
681
|
+
function homeDeleted(homeId) {
|
|
682
|
+
const agentId = agentIdOfHomeSession(homeId);
|
|
683
|
+
if (agentId) return agentId !== MIXY_AGENT_ID && agentPresence(agentId) === 'deleted';
|
|
684
|
+
const roomId = roomIdOfSession(homeId) || roomMemberOfSession(homeId)?.roomId || '';
|
|
685
|
+
return Boolean(roomId) && roomPresence(roomId) === 'deleted';
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** Retire the bookkeeping of deleted homes (and stale round counts). */
|
|
689
|
+
async function prune() {
|
|
690
|
+
await queue.prune((await queue.bookkeptHomes()).filter(homeDeleted));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Store `watch`; a home at its watch cap first gives up its oldest watches
|
|
694
|
+
* whose target sessions are proven deleted, and refuses the watch (so the
|
|
695
|
+
* send claims no report) when that frees too few. */
|
|
696
|
+
async function watchTask(watch) {
|
|
697
|
+
const own = (await queue.watches()).filter(
|
|
698
|
+
(kept) => kept?.homeSessionId === watch.homeSessionId && kept.runId !== watch.runId
|
|
699
|
+
);
|
|
700
|
+
const over = own.length + 1 - queue.watchCap;
|
|
701
|
+
const drop =
|
|
702
|
+
over > 0
|
|
703
|
+
? own
|
|
704
|
+
.filter((kept) => sessionDeleted(kept.taskSessionId))
|
|
705
|
+
.slice(0, over)
|
|
706
|
+
.map((kept) => kept.runId)
|
|
707
|
+
: [];
|
|
708
|
+
if (drop.length) {
|
|
709
|
+
log(`${watch.homeSessionId} is at its watch cap: dropped ${drop.length} watch(es) of deleted sessions`);
|
|
710
|
+
}
|
|
711
|
+
await queue.watch(watch, { drop });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Drain every queue left by an earlier process, after retiring stale bookkeeping. */
|
|
402
715
|
async function recover() {
|
|
716
|
+
try {
|
|
717
|
+
await prune();
|
|
718
|
+
} catch (error) {
|
|
719
|
+
log(`pruning the routine results failed: ${error?.message || error}`);
|
|
720
|
+
}
|
|
403
721
|
const homes = await queue.pendingHomes();
|
|
404
722
|
for (const homeId of homes) void drain(homeId);
|
|
405
723
|
return homes;
|
|
406
724
|
}
|
|
407
725
|
|
|
408
|
-
return {
|
|
726
|
+
return {
|
|
727
|
+
enqueue,
|
|
728
|
+
enqueueHeld,
|
|
729
|
+
enqueueTask,
|
|
730
|
+
announce,
|
|
731
|
+
drain,
|
|
732
|
+
recover,
|
|
733
|
+
watchTask,
|
|
734
|
+
unwatchTask: (runId) => queue.unwatch(runId),
|
|
735
|
+
taskWatches: () => queue.watches(),
|
|
736
|
+
resetRounds: (homeId) => queue.resetRounds(homeId),
|
|
737
|
+
};
|
|
409
738
|
}
|
|
410
739
|
|
|
411
740
|
let defaultDelivery = null;
|
|
412
741
|
/** The process delivery over the session store and data dir; built on first use. */
|
|
413
742
|
async function processDelivery() {
|
|
414
743
|
if (!defaultDelivery) {
|
|
415
|
-
const [
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
744
|
+
const [
|
|
745
|
+
{ loadSession, saveSessionAsync },
|
|
746
|
+
{ isSessionTurnInFlight },
|
|
747
|
+
{ getPluginData },
|
|
748
|
+
{ createAgentEditorApi },
|
|
749
|
+
{ probePath, PROBE_ABSENT },
|
|
750
|
+
{ sessionPath },
|
|
751
|
+
] = await Promise.all([
|
|
752
|
+
import('../runtime/agent/orchestrator/session/store.mjs'),
|
|
753
|
+
import('../runtime/agent/orchestrator/session/manager/runtime-liveness.mjs'),
|
|
754
|
+
import('../runtime/agent/orchestrator/config.mjs'),
|
|
755
|
+
import('./workflow-agents-api/agent-editor.mjs'),
|
|
756
|
+
import('../runtime/agent/orchestrator/session/store/fs-probe.mjs'),
|
|
757
|
+
import('../runtime/agent/orchestrator/session/store/session-paths.mjs'),
|
|
758
|
+
]);
|
|
422
759
|
// The editor's per-id definition probe: 'deleted' only when proven gone.
|
|
423
760
|
const { agentDefinitionPresence } = createAgentEditorApi({ cfgMod: { getPluginData } });
|
|
424
761
|
defaultDelivery = createAgentHomeDelivery({
|
|
@@ -427,6 +764,16 @@ async function processDelivery() {
|
|
|
427
764
|
turnInFlight: isSessionTurnInFlight,
|
|
428
765
|
queue: createRoutineResultQueue(join(getPluginData(), 'routine-results.json')),
|
|
429
766
|
agentPresence: (agentId) => agentDefinitionPresence([agentId])[agentId],
|
|
767
|
+
// Deleting a session unlinks its record (session-delete.mjs) and leaves
|
|
768
|
+
// no tombstone: only an absent file with no in-memory copy proves it.
|
|
769
|
+
// An unreadable file probes 'unreadable' and stays.
|
|
770
|
+
sessionDeleted: (sessionId) => {
|
|
771
|
+
try {
|
|
772
|
+
return probePath(sessionPath(sessionId)).state === PROBE_ABSENT && !loadSession(sessionId);
|
|
773
|
+
} catch {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
},
|
|
430
777
|
});
|
|
431
778
|
}
|
|
432
779
|
return defaultDelivery;
|
|
@@ -453,6 +800,26 @@ export async function deliverAgentTask(agentId, report) {
|
|
|
453
800
|
return (await processDelivery()).enqueueTask(agentId, report);
|
|
454
801
|
}
|
|
455
802
|
|
|
803
|
+
/** Keep one reporting turn's watch until its report is queued. */
|
|
804
|
+
export async function watchAgentTask(watch) {
|
|
805
|
+
return (await processDelivery()).watchTask(watch);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** Drop one reporting turn's watch (its turn was never taken). */
|
|
809
|
+
export async function unwatchAgentTask(runId) {
|
|
810
|
+
return (await processDelivery()).unwatchTask(runId);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** A user's own message in `homeId`: its round trips with every target restart. */
|
|
814
|
+
export async function resetAgentTaskRounds(homeId) {
|
|
815
|
+
return (await processDelivery()).resetRounds(homeId);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** The reporting turns whose report was not queued yet. */
|
|
819
|
+
export async function agentTaskWatches() {
|
|
820
|
+
return (await processDelivery()).taskWatches();
|
|
821
|
+
}
|
|
822
|
+
|
|
456
823
|
/** Daemon start: post the results an earlier process left queued. */
|
|
457
824
|
export async function recoverScheduleResults() {
|
|
458
825
|
return (await processDelivery()).recover();
|