mixdog 1.3.4 → 1.3.5
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/LICENSES/oh-my-pi-MIT.txt +23 -0
- package/NOTICE.md +15 -0
- package/README.md +11 -0
- package/package.json +2 -1
- package/scripts/embed-google-oauth-client.mjs +49 -0
- package/scripts/lib/test-spawn-ledger.mjs +46 -1
- package/scripts/test.mjs +5 -0
- package/src/defaults/shipped-history.json +8 -0
- package/src/defaults/skills/browser-use/SKILL.md +21 -9
- package/src/defaults/skills/setup/SKILL.md +7 -2
- package/src/defaults/skills/setup/references/actions.md +82 -27
- package/src/defaults/skills/setup/references/surfaces.md +67 -34
- package/src/defaults/skills/setup-guide/SKILL.md +3 -3
- package/src/lib/keychain-cjs.cjs +359 -29
- package/src/rules/agent-mode/AGENT_MODE.md +10 -2
- package/src/rules/agent-mode/MIXY.md +12 -0
- package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
- package/src/runtime/agent/orchestrator/config.mjs +18 -0
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
- package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
- package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
- package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
- package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
- package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
- package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
- package/src/runtime/channels/lib/scheduler.mjs +109 -14
- package/src/runtime/channels/lib/schedules-db.mjs +49 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
- package/src/runtime/channels/lib/worker-main.mjs +9 -0
- package/src/runtime/office/com/com-adapter.mjs +17 -1
- package/src/runtime/office/com/office-com-host.ps1 +51 -1
- package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
- package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
- package/src/runtime/office/pdf/document-preview.mjs +326 -8
- package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
- package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
- package/src/runtime/office/portable/portable-soffice.mjs +45 -5
- package/src/runtime/shared/bot-hops.mjs +62 -2
- package/src/runtime/shared/config.mjs +49 -22
- package/src/runtime/shared/connector-catalog.d.mts +4 -2
- package/src/runtime/shared/connector-catalog.mjs +16 -8
- package/src/runtime/shared/developer-options.mjs +32 -1
- package/src/runtime/shared/google-oauth-client.generated.json +4 -0
- package/src/runtime/shared/google-oauth-client.mjs +34 -9
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
- package/src/runtime/shared/llm/well-formed.mjs +66 -0
- package/src/runtime/shared/provider-accounts.mjs +224 -58
- package/src/runtime/shared/provider-api-key.mjs +19 -8
- package/src/runtime/shared/routine-trigger.mjs +42 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -0
- package/src/runtime/shared/transcript-metadata.mjs +38 -0
- package/src/runtime/shared/user-data-guard.mjs +30 -2
- package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
- package/src/session-runtime/agent-home-delivery.mjs +57 -13
- package/src/session-runtime/agent-room-delivery.mjs +28 -1
- package/src/session-runtime/agent-task-sessions.mjs +292 -79
- package/src/session-runtime/boot/begin.mjs +3 -2
- package/src/session-runtime/boot/tools.mjs +2 -0
- package/src/session-runtime/bot-chain.mjs +32 -0
- package/src/session-runtime/bot-status.mjs +7 -4
- package/src/session-runtime/channel-config-api.mjs +63 -17
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
- package/src/session-runtime/internal-tool-executor.mjs +4 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
- package/src/session-runtime/mixy-heartbeat.mjs +807 -0
- package/src/session-runtime/model-route/route-tuning.mjs +10 -6
- package/src/session-runtime/owned-automation-run.mjs +33 -12
- package/src/session-runtime/provider-auth/login.mjs +7 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
- package/src/session-runtime/routine-home-turn.mjs +33 -6
- package/src/session-runtime/schedule-session-run.mjs +7 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
- package/src/session-runtime/services/channel-admin.mjs +66 -18
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +128 -25
- package/src/session-runtime/services/schedule-access.mjs +14 -0
- package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
- package/src/session-runtime/session-create/session-options.mjs +7 -4
- package/src/session-runtime/settings-system-api.mjs +16 -1
- package/src/session-runtime/setup-tool/executor.mjs +49 -14
- package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
- package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
- package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
- package/src/session-runtime/task-session-route.mjs +99 -0
- package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
- package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
- package/src/session-runtime/tool-surface.mjs +4 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
- package/src/session-runtime/workspace/transitions.mjs +10 -2
- package/src/standalone/daemon-session-control.mjs +88 -20
- package/src/standalone/daemon.mjs +8 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
- package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
- package/src/standalone/session-service.mjs +13 -0
- package/src/standalone/session-tool-output.mjs +8 -5
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/dist/index.mjs +24 -2
- package/src/tui/session/notice-surface.mjs +17 -1
- package/src/tui/session/session-api/integrations.mjs +1 -0
- package/src/tui/session/session-api/lifecycle.mjs +8 -0
- package/src/tui/session/session-api/settings.mjs +14 -2
- package/src/tui/session/turn-stream.mjs +17 -1
- package/src/tui/session/turn.mjs +22 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// session/message-reactions.mjs
|
|
2
|
+
// A bot's emoji reaction on a message the user wrote, shown under the user's
|
|
3
|
+
// bubble. It is stored on that message (`meta.transcript.reactions`, one
|
|
4
|
+
// `{ agentId, emoji, at }` per bot), so it travels with the transcript: a
|
|
5
|
+
// reload, a restart and every synced client read it from there. In a group
|
|
6
|
+
// room the message is the user's row in the room's log, and each member's
|
|
7
|
+
// reaction is kept beside the others'.
|
|
8
|
+
import { sessionMessageText } from '../runtime-core/session-text.mjs';
|
|
9
|
+
import { isInternalTranscriptDisplayText } from '../../../shared/tool-execution-contract.mjs';
|
|
10
|
+
import { normalizeMessageReactions } from '../../../shared/transcript-metadata.mjs';
|
|
11
|
+
import { isRoomRowMessage } from './agent-room.mjs';
|
|
12
|
+
|
|
13
|
+
/** Whether `message` is one the user wrote: a prompt typed (or steered) into
|
|
14
|
+
* the chat, or the user's own row in a room's log. Runtime notices, routine
|
|
15
|
+
* fires, posted results and other bots' messages are not. */
|
|
16
|
+
export function isReactableMessage(message) {
|
|
17
|
+
if (message?.role !== 'user' || !message.meta || typeof message.meta !== 'object') return false;
|
|
18
|
+
const { meta } = message;
|
|
19
|
+
const transcript = meta.transcript;
|
|
20
|
+
if (typeof transcript?.messageId !== 'string' || !transcript.messageId) return false;
|
|
21
|
+
if (isRoomRowMessage(message)) return meta.room.kind === 'user' && !meta.room.fromSession;
|
|
22
|
+
if ((meta.source && meta.source !== 'steering') || meta.synthetic === true) return false;
|
|
23
|
+
if (transcript.fromAgent || transcript.routineTrigger) return false;
|
|
24
|
+
return !isInternalTranscriptDisplayText(sessionMessageText(message.content));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Set `agentId`'s reaction (`emoji`; '' removes it) on the user message
|
|
29
|
+
* `messagesBack` user messages before the newest one in `messages`. One
|
|
30
|
+
* reaction per bot: a new one takes the place of the old. `othersKeep`: an
|
|
31
|
+
* emoji another bot already left there is refused (a group room's members do
|
|
32
|
+
* not repeat each other). Returns `{ previous, message, messageId, reactions,
|
|
33
|
+
* changed }`; `message` is a new object (the store's delta saves see a change
|
|
34
|
+
* by identity), placed by the caller. Throws when there is no such message.
|
|
35
|
+
*/
|
|
36
|
+
export function reactToMessage(messages, { agentId, emoji, messagesBack = 0, othersKeep = false, at = Date.now() }) {
|
|
37
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
38
|
+
let seen = 0;
|
|
39
|
+
let previous = null;
|
|
40
|
+
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
41
|
+
if (!isReactableMessage(list[index])) continue;
|
|
42
|
+
if (seen === messagesBack) {
|
|
43
|
+
previous = list[index];
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
seen += 1;
|
|
47
|
+
}
|
|
48
|
+
if (!previous) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
seen ? `react: there are only ${seen} user messages to react to` : 'react: there is no user message to react to'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const transcript = previous.meta.transcript;
|
|
54
|
+
const current = normalizeMessageReactions(transcript.reactions);
|
|
55
|
+
const own = current.find((entry) => entry.agentId === agentId);
|
|
56
|
+
if (emoji && othersKeep) {
|
|
57
|
+
const taken = current.find((entry) => entry.agentId !== agentId && entry.emoji === emoji);
|
|
58
|
+
if (taken) throw new Error(`react: ${taken.agentId} already left ${emoji} on that message`);
|
|
59
|
+
}
|
|
60
|
+
const messageId = transcript.messageId;
|
|
61
|
+
if (own?.emoji === emoji || (!own && !emoji)) {
|
|
62
|
+
return { previous, message: previous, messageId, reactions: current, changed: false };
|
|
63
|
+
}
|
|
64
|
+
const entry = { agentId, emoji, at };
|
|
65
|
+
let reactions;
|
|
66
|
+
if (!emoji) reactions = current.filter((each) => each.agentId !== agentId);
|
|
67
|
+
else if (own) reactions = current.map((each) => (each.agentId === agentId ? entry : each));
|
|
68
|
+
else reactions = [...current, entry];
|
|
69
|
+
const { reactions: _old, ...rest } = transcript;
|
|
70
|
+
const message = {
|
|
71
|
+
...previous,
|
|
72
|
+
meta: { ...previous.meta, transcript: reactions.length ? { ...rest, reactions } : rest },
|
|
73
|
+
};
|
|
74
|
+
return { previous, message, messageId, reactions, changed: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** reactToMessage on a stored session: the running turn's working transcript
|
|
78
|
+
* when a turn is in flight (its end commits that transcript), and the
|
|
79
|
+
* session's own message list, both holding the replaced message. */
|
|
80
|
+
export function reactInSession(session, change) {
|
|
81
|
+
const working = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
|
|
82
|
+
const result = reactToMessage(working || session?.messages, change);
|
|
83
|
+
if (!result.changed) return result;
|
|
84
|
+
for (const list of [working, session.messages]) {
|
|
85
|
+
const index = Array.isArray(list) ? list.indexOf(result.previous) : -1;
|
|
86
|
+
if (index >= 0) list[index] = result.message;
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// A real agent-loop turn in a bot's home chat whose model first calls `react`
|
|
2
|
+
// with `args`, then answers with empty text (and, if nudged, with 'Sure.').
|
|
3
|
+
// The react call runs through the real tool batch and the real react handler,
|
|
4
|
+
// so its tool result is the one a live turn would see.
|
|
5
|
+
import { agentLoop } from './agent-loop.mjs';
|
|
6
|
+
import { setInternalToolsProvider } from '../internal-tools.mjs';
|
|
7
|
+
import { REACTION_TOOL_DEFS } from '../runtime-core/reaction-tool-defs.mjs';
|
|
8
|
+
import { createReactionToolHandlers } from '../../../../session-runtime/internal-tool-executor/reaction-tool.mjs';
|
|
9
|
+
|
|
10
|
+
let scopeSeq = 0;
|
|
11
|
+
|
|
12
|
+
// `reply`: the text the model answers with after reacting (default none).
|
|
13
|
+
// `nextPrompt`: a second user turn run afterwards in the same session, whose
|
|
14
|
+
// model answers 'Next.' — the provider prefix guard carries across both.
|
|
15
|
+
export async function runReactionTurn(
|
|
16
|
+
args,
|
|
17
|
+
{ onToolCall = null, onToolResult = () => {}, reply = '', nextPrompt = '' } = {}
|
|
18
|
+
) {
|
|
19
|
+
const scopeId = `reaction-turn-${process.pid}-${++scopeSeq}`;
|
|
20
|
+
const prompt = { role: 'user', content: '고마워', meta: { transcript: { at: 1, messageId: 'msg_user_1' } } };
|
|
21
|
+
const session = {
|
|
22
|
+
id: 'sess_home_reviewer',
|
|
23
|
+
owner: 'cli',
|
|
24
|
+
agent: 'lead',
|
|
25
|
+
mcpScopeId: scopeId,
|
|
26
|
+
tools: [...REACTION_TOOL_DEFS],
|
|
27
|
+
messages: [prompt],
|
|
28
|
+
contextWindow: 200_000,
|
|
29
|
+
rawContextWindow: 200_000,
|
|
30
|
+
compaction: { auto: false },
|
|
31
|
+
};
|
|
32
|
+
const { react } = createReactionToolHandlers({ rt: { session }, live: () => null });
|
|
33
|
+
const dispose = setInternalToolsProvider({
|
|
34
|
+
scopeId,
|
|
35
|
+
tools: [...REACTION_TOOL_DEFS],
|
|
36
|
+
executor: (_name, toolArgs, callerCtx) => react(toolArgs, { callerCtx }),
|
|
37
|
+
});
|
|
38
|
+
const results = [];
|
|
39
|
+
let sends = 0;
|
|
40
|
+
const provider = {
|
|
41
|
+
async send() {
|
|
42
|
+
sends += 1;
|
|
43
|
+
if (sends === 1) return { content: '', toolCalls: [{ id: 'call_react', name: 'react', arguments: args }] };
|
|
44
|
+
if (sends > 3 || (reply && sends === 3)) return { content: 'Next.', toolCalls: [], stopReason: 'end_turn' };
|
|
45
|
+
return { content: sends === 2 ? reply : 'Sure.', toolCalls: [], stopReason: 'end_turn' };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const loopOpts = {
|
|
49
|
+
session,
|
|
50
|
+
sessionId: session.id,
|
|
51
|
+
onToolResult: (message) => {
|
|
52
|
+
results.push(message);
|
|
53
|
+
onToolResult(message);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
// The turn's working transcript is the loop's own list, as in a live turn.
|
|
58
|
+
const working = [prompt];
|
|
59
|
+
session.liveTurnMessages = working;
|
|
60
|
+
const result = await agentLoop(provider, working, 'fake-model', session.tools, onToolCall, process.cwd(), loopOpts);
|
|
61
|
+
session.messages = [...working];
|
|
62
|
+
delete session.liveTurnMessages;
|
|
63
|
+
let next = null;
|
|
64
|
+
if (nextPrompt) {
|
|
65
|
+
if (result.content) session.messages.push({ role: 'assistant', content: result.content });
|
|
66
|
+
session.messages.push({ role: 'user', content: nextPrompt });
|
|
67
|
+
session.liveTurnMessages = session.messages;
|
|
68
|
+
next = await agentLoop(provider, session.messages, 'fake-model', session.tools, null, process.cwd(), loopOpts);
|
|
69
|
+
}
|
|
70
|
+
const user = session.messages.find((message) => message.role === 'user');
|
|
71
|
+
return { result, next, sends, results, reactions: user?.meta?.transcript?.reactions || [] };
|
|
72
|
+
} finally {
|
|
73
|
+
dispose();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -35,12 +35,10 @@ export interface RoutineNotice {
|
|
|
35
35
|
}>;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export const ROUTINE_RESULT_SOURCE: 'schedule-result';
|
|
39
38
|
export const TASK_LOOP_CAP: number;
|
|
40
39
|
export function routineSummary(text: unknown): string;
|
|
41
40
|
export const REPLY_LIMIT: number;
|
|
42
41
|
export function boundedReply(text: unknown): { text: string; chars: number };
|
|
43
|
-
export const WAKE_TEXT_BUDGET: number;
|
|
44
42
|
export function routineResultMessage(options: {
|
|
45
43
|
kind?: 'schedule' | 'webhook';
|
|
46
44
|
name: string;
|
|
@@ -13,7 +13,7 @@ import { artifactLines, artifactRecords, normalizeHandoffFiles } from '../../../
|
|
|
13
13
|
import { transcriptAttachmentFields } from '../../../shared/transcript-metadata.mjs';
|
|
14
14
|
import { agentIdOfHomeSession } from './agent-home.mjs';
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
const ROUTINE_RESULT_SOURCE = 'schedule-result';
|
|
17
17
|
const SUMMARY_LIMIT = 600;
|
|
18
18
|
|
|
19
19
|
/** The one-line, bounded summary a routine result keeps of its run output. */
|
|
@@ -206,6 +206,8 @@ export function agentTaskMessage({
|
|
|
206
206
|
files = [],
|
|
207
207
|
omitted = 0,
|
|
208
208
|
hop = 0,
|
|
209
|
+
sendId = '',
|
|
210
|
+
sendIds = [],
|
|
209
211
|
at = Date.now(),
|
|
210
212
|
messageId,
|
|
211
213
|
}) {
|
|
@@ -260,6 +262,11 @@ export function agentTaskMessage({
|
|
|
260
262
|
...(agentIdOfHomeSession(taskSessionId) ? { agentId: agentIdOfHomeSession(taskSessionId) } : {}),
|
|
261
263
|
// Bot-to-bot chain position of the send this answers (session-runtime/bot-hops.mjs).
|
|
262
264
|
...(hop > 0 ? { hop } : {}),
|
|
265
|
+
// The send this report answers (the send result's `sendId`).
|
|
266
|
+
...(sendId ? { sendId } : {}),
|
|
267
|
+
// Every send of a turn that answered several (folded into one turn):
|
|
268
|
+
// the one report ends all of them, `sendId` being the first.
|
|
269
|
+
...(sendIds.length > 1 ? { sendIds } : {}),
|
|
263
270
|
...(produced.length ? { files: produced } : {}),
|
|
264
271
|
},
|
|
265
272
|
transcript: { at, ...(messageId ? { messageId } : {}), ...(attachments.length ? { attachments } : {}) },
|
|
@@ -268,7 +275,7 @@ export function agentTaskMessage({
|
|
|
268
275
|
}
|
|
269
276
|
|
|
270
277
|
/** Reply text one wake message carries across all its reports. */
|
|
271
|
-
|
|
278
|
+
const WAKE_TEXT_BUDGET = 24000;
|
|
272
279
|
|
|
273
280
|
/** A task report's content with its reply replaced by the row's one-line
|
|
274
281
|
* summary and a pointer to the session; null for any other message. */
|
|
@@ -385,6 +392,8 @@ export function routineResultNotice(message) {
|
|
|
385
392
|
sessionId,
|
|
386
393
|
...(session ? { target: 'session' } : {}),
|
|
387
394
|
...(agentId ? { agentId } : {}),
|
|
395
|
+
...(routine.sendId ? { sendId: routine.sendId } : {}),
|
|
396
|
+
...(routine.sendIds?.length > 1 ? { sendIds: routine.sendIds } : {}),
|
|
388
397
|
...(files.length ? { files } : {}),
|
|
389
398
|
},
|
|
390
399
|
// The stored files, in the field a prompt row draws its attachments from.
|
|
@@ -252,6 +252,10 @@ export async function sendWithRecovery(ctx) {
|
|
|
252
252
|
retryAttemptNumber: Math.max(0, Number(transportRetriesUsed) || 0),
|
|
253
253
|
retryMaxForDisplay,
|
|
254
254
|
});
|
|
255
|
+
// The tail reminder this request carries; every assistant turn committed from
|
|
256
|
+
// its response stores it (turnReminder) so a provider in turn-scoped mode can
|
|
257
|
+
// re-send it verbatim before that turn (tail-reminder-split.mjs).
|
|
258
|
+
let turnReminder = '';
|
|
255
259
|
try {
|
|
256
260
|
let response;
|
|
257
261
|
try {
|
|
@@ -266,22 +270,36 @@ export async function sendWithRecovery(ctx) {
|
|
|
266
270
|
// Ephemeral tail reminder (tail-reminder-rules.mjs): added to this request's
|
|
267
271
|
// copy only, never to the stored transcript.
|
|
268
272
|
const requestMessages = withTailReminder(messages, { session: ctx.sessionRef, provider, model });
|
|
273
|
+
if (requestMessages !== messages) turnReminder = String(requestMessages.at(-1).content);
|
|
269
274
|
response = await provider.send(requestMessages, model, sendTools.length ? sendTools : undefined, admittedOpts);
|
|
270
275
|
hooks.logTurnTiming('ok');
|
|
271
276
|
} catch (sendErr) {
|
|
272
277
|
hooks.logTurnTiming(`err:${sendErr?.code || sendErr?.name || 'unknown'}`);
|
|
273
278
|
stampStreamOutcome(sendErr, hooks.relayWitness);
|
|
274
|
-
return
|
|
279
|
+
return withTurnReminder(
|
|
280
|
+
await recoverFromSendError(sendErr, { ...ctx, relayWitness: hooks.relayWitness }),
|
|
281
|
+
turnReminder
|
|
282
|
+
);
|
|
275
283
|
}
|
|
276
284
|
// streamedChars: what this send put on screen through onTextDelta (net of
|
|
277
285
|
// acknowledged retractions), so the loop can withdraw it if it discards
|
|
278
286
|
// the response and re-sends.
|
|
279
|
-
return
|
|
287
|
+
return withTurnReminder(
|
|
288
|
+
{ action: 'proceed', response, streamedChars: hooks.relayWitness.text.length },
|
|
289
|
+
turnReminder
|
|
290
|
+
);
|
|
280
291
|
} finally {
|
|
281
292
|
hooks.restore();
|
|
282
293
|
}
|
|
283
294
|
}
|
|
284
295
|
|
|
296
|
+
// A proceeding response — complete, promoted incomplete, recovered tool calls
|
|
297
|
+
// or a cut kept as shown — was generated under this request's reminder.
|
|
298
|
+
function withTurnReminder(result, turnReminder) {
|
|
299
|
+
if (turnReminder && result?.action === 'proceed' && result.response) result.response.turnReminder = turnReminder;
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
285
303
|
function loopTag({ sessionId, nextIteration }) {
|
|
286
304
|
return `sess=${sessionId || 'unknown'} iter=${nextIteration}`;
|
|
287
305
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted copy of the agent pool's session-header cache. The in-memory
|
|
3
|
+
* cache of store-agent-worker-rows.mjs died with the daemon, so the first
|
|
4
|
+
* pool listing after every restart re-read every worker transcript (~1,700
|
|
5
|
+
* records, ~1 GB) just to rebuild headers that had not changed: seconds of
|
|
6
|
+
* I/O and CPU right when the restored conversation was loading.
|
|
7
|
+
*
|
|
8
|
+
* The file only seeds the in-memory cache. Every seeded entry is still served
|
|
9
|
+
* under that cache's full-stamp policy — the record must still have the exact
|
|
10
|
+
* { dev, ino, size, mtimeNs, ctimeNs } its header was read from, a stamp that
|
|
11
|
+
* was settled when cached — so a record changed while no daemon ran (or by
|
|
12
|
+
* another process) is read again. Being a cache, an absent, unreadable,
|
|
13
|
+
* damaged, other-version or other-field-set file seeds nothing (a bad entry
|
|
14
|
+
* alone is skipped) and is replaced by the next write; writes are atomic.
|
|
15
|
+
*/
|
|
16
|
+
import { randomBytes } from 'node:crypto';
|
|
17
|
+
import { readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { sessionStampOf } from './store/canonical-reader.mjs';
|
|
20
|
+
import { isStoredSessionId } from './store-summary-fields.mjs';
|
|
21
|
+
import { dataDir } from './store-summary-locations.mjs';
|
|
22
|
+
|
|
23
|
+
const VERSION = 1;
|
|
24
|
+
const STAMP_KEYS = ['dev', 'ino', 'size', 'mtimeNs', 'ctimeNs'];
|
|
25
|
+
const UNSIGNED = /^\d+$/;
|
|
26
|
+
|
|
27
|
+
export function storedAgentWorkerHeaderCachePath(dir = dataDir()) {
|
|
28
|
+
return join(dir, 'agent-worker-header-cache.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function decodeStamp(value) {
|
|
32
|
+
if (!Array.isArray(value) || value.length !== STAMP_KEYS.length) return null;
|
|
33
|
+
if (!value.every((part) => typeof part === 'string' && UNSIGNED.test(part))) return null;
|
|
34
|
+
return sessionStampOf(Object.fromEntries(STAMP_KEYS.map((key, index) => [key, BigInt(value[index])])));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function decodeHeader(value, fields) {
|
|
38
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
39
|
+
return Object.keys(value).every((key) => fields.has(key)) ? value : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** sessionId -> { stamp, header } from the file at `path`; empty when the
|
|
43
|
+
* file is absent, unreadable, damaged, or written for another version or
|
|
44
|
+
* header field list. */
|
|
45
|
+
export async function readPersistedWorkerHeaders(path, fields) {
|
|
46
|
+
const entries = new Map();
|
|
47
|
+
let doc;
|
|
48
|
+
try {
|
|
49
|
+
doc = JSON.parse(await readFile(path, 'utf8'));
|
|
50
|
+
} catch {
|
|
51
|
+
return entries;
|
|
52
|
+
}
|
|
53
|
+
if (doc?.version !== VERSION || doc.fields !== fields.join(',')) return entries;
|
|
54
|
+
if (!doc.sessions || typeof doc.sessions !== 'object' || Array.isArray(doc.sessions)) return entries;
|
|
55
|
+
const wanted = new Set(fields);
|
|
56
|
+
for (const [sessionId, entry] of Object.entries(doc.sessions)) {
|
|
57
|
+
if (!isStoredSessionId(sessionId) || !entry || typeof entry !== 'object') continue;
|
|
58
|
+
const stamp = decodeStamp(entry.stamp);
|
|
59
|
+
const header = decodeHeader(entry.header, wanted);
|
|
60
|
+
if (stamp && header) entries.set(sessionId, { stamp, header });
|
|
61
|
+
}
|
|
62
|
+
return entries;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Replaces the file at `path` with `entries` (sessionId -> { stamp, header })
|
|
66
|
+
* by rename, so a reader sees the old file or the new one, never a torn one.
|
|
67
|
+
* Resolves false instead of throwing: a lost write only costs the next
|
|
68
|
+
* restart some re-reads. The data directory is never created: a store that
|
|
69
|
+
* is gone gets no cache. */
|
|
70
|
+
export async function writePersistedWorkerHeaders(path, fields, entries) {
|
|
71
|
+
const sessions = {};
|
|
72
|
+
for (const [sessionId, { stamp, header }] of entries) {
|
|
73
|
+
sessions[sessionId] = { stamp: STAMP_KEYS.map((key) => String(stamp[key])), header };
|
|
74
|
+
}
|
|
75
|
+
// The temp shape is atomic-file.mjs's `<name>.mixdog-tmp-<hex>`.
|
|
76
|
+
const tmp = `${path}.mixdog-tmp-${randomBytes(8).toString('hex')}`;
|
|
77
|
+
try {
|
|
78
|
+
await writeFile(tmp, JSON.stringify({ version: VERSION, fields: fields.join(','), sessions }), { flag: 'wx' });
|
|
79
|
+
await rename(tmp, path);
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
await unlink(tmp).catch(() => {});
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { readFileSync } from 'node:fs';
|
|
19
19
|
import { readFile, stat } from 'node:fs/promises';
|
|
20
|
-
import { join } from 'node:path';
|
|
20
|
+
import { basename, join } from 'node:path';
|
|
21
21
|
import {
|
|
22
22
|
isSettledSessionStamp,
|
|
23
23
|
sameSessionStamp,
|
|
@@ -27,6 +27,11 @@ import {
|
|
|
27
27
|
import { readJsonProjection } from '../../../shared/pending-json-projection.mjs';
|
|
28
28
|
import { listStoredAgentWorkers as assembleStoredAgentWorkers } from './store-agent-worker-pool.mjs';
|
|
29
29
|
import { readTopLevelFields } from './store-agent-worker-header.mjs';
|
|
30
|
+
import {
|
|
31
|
+
readPersistedWorkerHeaders,
|
|
32
|
+
storedAgentWorkerHeaderCachePath,
|
|
33
|
+
writePersistedWorkerHeaders,
|
|
34
|
+
} from './store-agent-worker-header-cache.mjs';
|
|
30
35
|
import {
|
|
31
36
|
TAG_TOMBSTONE_TTL_MS,
|
|
32
37
|
agentPaneVisible,
|
|
@@ -330,8 +335,104 @@ const WORKER_HEADER_FIELDS = [
|
|
|
330
335
|
// assembly the first answer stands, absent or unreadable included.
|
|
331
336
|
let passHeaders = null; // path -> header | null, live only during an assembly
|
|
332
337
|
|
|
333
|
-
function workerSessionHeaderPath(sessionId) {
|
|
334
|
-
return join(
|
|
338
|
+
function workerSessionHeaderPath(sessionId, dir = dataDir()) {
|
|
339
|
+
return join(dir, 'sessions', `${sessionId}.json`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Every change to the header cache is counted, so the persisted copy is
|
|
343
|
+
// rewritten only when the cache it mirrors moved.
|
|
344
|
+
let headerCacheEdits = 0;
|
|
345
|
+
|
|
346
|
+
function cacheHeader(path, entry) {
|
|
347
|
+
workerSessionHeaderCache.set(path, entry);
|
|
348
|
+
headerCacheEdits += 1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function dropHeader(path) {
|
|
352
|
+
if (workerSessionHeaderCache.delete(path)) headerCacheEdits += 1;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// The header cache outlives the daemon through store-agent-worker-header-
|
|
356
|
+
// cache.mjs: the first awaited listing of a data directory seeds the cache
|
|
357
|
+
// from its file (entries already in memory win), and an awaited listing that
|
|
358
|
+
// changed the cache schedules a rewrite. The write waits out
|
|
359
|
+
// HEADER_PERSIST_DELAY_MS — off the boot path, and at most one write per
|
|
360
|
+
// window however often agents churn. Seeded entries pass the same full-stamp
|
|
361
|
+
// check as any other, so a seed never serves a header its file no longer has.
|
|
362
|
+
const HEADER_PERSIST_DELAY_MS = 10_000;
|
|
363
|
+
let seededDir = null;
|
|
364
|
+
let seeding = null;
|
|
365
|
+
// The edit count the last write left on disk, and the directory it went to:
|
|
366
|
+
// a count persisted for one data directory says nothing about another.
|
|
367
|
+
let persistedEdits = 0;
|
|
368
|
+
let persistedDir = null;
|
|
369
|
+
let persistTimer = null;
|
|
370
|
+
let persistTimerDir = null;
|
|
371
|
+
let persistWrite = null;
|
|
372
|
+
|
|
373
|
+
function seedPersistedHeaders() {
|
|
374
|
+
const dir = dataDir();
|
|
375
|
+
if (seededDir !== dir) {
|
|
376
|
+
seededDir = dir;
|
|
377
|
+
seeding = readPersistedWorkerHeaders(storedAgentWorkerHeaderCachePath(dir), WORKER_HEADER_FIELDS).then(
|
|
378
|
+
(entries) => {
|
|
379
|
+
for (const [sessionId, { stamp, header }] of entries) {
|
|
380
|
+
const path = workerSessionHeaderPath(sessionId, dir);
|
|
381
|
+
if (!workerSessionHeaderCache.has(path)) workerSessionHeaderCache.set(path, { stamp, header, pass: 0 });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return seeding;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function schedulePersistHeaders() {
|
|
390
|
+
const dir = dataDir();
|
|
391
|
+
// A write pending for the directory this listing left is dropped: the
|
|
392
|
+
// listing just forgot that directory's unread headers, so writing it now
|
|
393
|
+
// would shrink its file. Its last written cache still stands.
|
|
394
|
+
if (persistTimer && persistTimerDir !== dir) {
|
|
395
|
+
clearTimeout(persistTimer);
|
|
396
|
+
persistTimer = null;
|
|
397
|
+
}
|
|
398
|
+
if (persistTimer || (persistedDir === dir && headerCacheEdits === persistedEdits)) return;
|
|
399
|
+
persistTimerDir = dir;
|
|
400
|
+
persistTimer = setTimeout(() => {
|
|
401
|
+
persistTimer = null;
|
|
402
|
+
persistHeaders(persistTimerDir);
|
|
403
|
+
}, HEADER_PERSIST_DELAY_MS);
|
|
404
|
+
persistTimer.unref?.();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function persistHeaders(dir) {
|
|
408
|
+
if (persistWrite) return persistWrite;
|
|
409
|
+
const edits = headerCacheEdits;
|
|
410
|
+
const entries = [];
|
|
411
|
+
for (const [path, entry] of workerSessionHeaderCache) {
|
|
412
|
+
const sessionId = basename(path, '.json');
|
|
413
|
+
if (isStoredSessionId(sessionId) && workerSessionHeaderPath(sessionId, dir) === path) {
|
|
414
|
+
entries.push([sessionId, entry]);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
persistWrite = writePersistedWorkerHeaders(storedAgentWorkerHeaderCachePath(dir), WORKER_HEADER_FIELDS, entries).then(
|
|
418
|
+
(written) => {
|
|
419
|
+
if (written) {
|
|
420
|
+
persistedEdits = edits;
|
|
421
|
+
persistedDir = dir;
|
|
422
|
+
}
|
|
423
|
+
persistWrite = null;
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
return persistWrite;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Test/diagnostic seam: writes a scheduled header-cache persist now and
|
|
430
|
+
* resolves once no write is in flight. */
|
|
431
|
+
export function flushStoredAgentWorkerHeaderCache() {
|
|
432
|
+
if (!persistTimer) return persistWrite || Promise.resolve();
|
|
433
|
+
clearTimeout(persistTimer);
|
|
434
|
+
persistTimer = null;
|
|
435
|
+
return persistHeaders(persistTimerDir);
|
|
335
436
|
}
|
|
336
437
|
|
|
337
438
|
function readWorkerSessionHeader(sessionId) {
|
|
@@ -358,15 +459,18 @@ function observeStamp(path) {
|
|
|
358
459
|
// window when read. Size + mtime alone let an equal-length rewrite that kept
|
|
359
460
|
// its mtime serve stale ownership, status and cancel fields.
|
|
360
461
|
function probeWorkerSessionHeader(path) {
|
|
361
|
-
// Absent and unreadable alike: no header.
|
|
462
|
+
// Absent and unreadable alike: no header, and no cached one kept for it.
|
|
362
463
|
const before = observeStamp(path);
|
|
363
|
-
if (!before)
|
|
464
|
+
if (!before) {
|
|
465
|
+
dropHeader(path);
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
364
468
|
const cached = workerSessionHeaderCache.get(path);
|
|
365
469
|
if (cached && sameSessionStamp(cached.stamp, before)) {
|
|
366
470
|
cached.pass = headerPass;
|
|
367
471
|
return cached.header;
|
|
368
472
|
}
|
|
369
|
-
|
|
473
|
+
dropHeader(path);
|
|
370
474
|
// A rewrite re-reads only this record, and only its top-level fields are
|
|
371
475
|
// decoded: the transcript bytes are validated, never materialized.
|
|
372
476
|
let header = null;
|
|
@@ -378,7 +482,7 @@ function probeWorkerSessionHeader(path) {
|
|
|
378
482
|
if (!header) return null;
|
|
379
483
|
workerHeaderReads += 1;
|
|
380
484
|
if (isSettledSessionStamp(before) && sameSessionStamp(before, observeStamp(path))) {
|
|
381
|
-
|
|
485
|
+
cacheHeader(path, { stamp: before, header, pass: headerPass });
|
|
382
486
|
}
|
|
383
487
|
return header;
|
|
384
488
|
}
|
|
@@ -395,10 +499,13 @@ async function observeStampAsync(path) {
|
|
|
395
499
|
* same full-stamp cache policy. */
|
|
396
500
|
async function probeWorkerSessionHeaderAsync(path) {
|
|
397
501
|
const before = await observeStampAsync(path);
|
|
398
|
-
if (!before)
|
|
502
|
+
if (!before) {
|
|
503
|
+
dropHeader(path);
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
399
506
|
const cached = workerSessionHeaderCache.get(path);
|
|
400
507
|
if (cached && sameSessionStamp(cached.stamp, before)) return cached.header;
|
|
401
|
-
|
|
508
|
+
dropHeader(path);
|
|
402
509
|
let header = null;
|
|
403
510
|
try {
|
|
404
511
|
header = readTopLevelFields(await readFile(path), WORKER_HEADER_FIELDS);
|
|
@@ -408,7 +515,7 @@ async function probeWorkerSessionHeaderAsync(path) {
|
|
|
408
515
|
if (!header) return null;
|
|
409
516
|
workerHeaderReads += 1;
|
|
410
517
|
if (isSettledSessionStamp(before) && sameSessionStamp(before, await observeStampAsync(path))) {
|
|
411
|
-
|
|
518
|
+
cacheHeader(path, { stamp: before, header, pass: headerPass });
|
|
412
519
|
}
|
|
413
520
|
return header;
|
|
414
521
|
}
|
|
@@ -418,7 +525,9 @@ async function probeWorkerSessionHeaderAsync(path) {
|
|
|
418
525
|
const HEADER_READ_CONCURRENCY = 4;
|
|
419
526
|
|
|
420
527
|
async function prefetchWorkerHeaders(sessionIds, headers) {
|
|
421
|
-
const paths = [...new Set([...sessionIds].map(
|
|
528
|
+
const paths = [...new Set([...sessionIds].map((sessionId) => workerSessionHeaderPath(sessionId)))].filter(
|
|
529
|
+
(path) => !headers.has(path)
|
|
530
|
+
);
|
|
422
531
|
let next = 0;
|
|
423
532
|
const drain = async () => {
|
|
424
533
|
while (next < paths.length) {
|
|
@@ -634,7 +743,7 @@ export function listStoredAgentWorkers({ runningSessionIds = null } = {}) {
|
|
|
634
743
|
// Keep what the latest assembly read, whatever the index size; forget the rest.
|
|
635
744
|
function forgetUnreadHeaders() {
|
|
636
745
|
for (const [path, entry] of workerSessionHeaderCache) {
|
|
637
|
-
if (entry.pass !== headerPass)
|
|
746
|
+
if (entry.pass !== headerPass) dropHeader(path);
|
|
638
747
|
}
|
|
639
748
|
}
|
|
640
749
|
|
|
@@ -643,6 +752,8 @@ function forgetUnreadHeaders() {
|
|
|
643
752
|
* each Lead row's, and each child's owner) are read first with awaited
|
|
644
753
|
* stats and reads; the assembly then runs over what was read, with no file
|
|
645
754
|
* access left in it (any header it still asks for falls back to a read).
|
|
755
|
+
* The first listing after a restart seeds the header cache from its
|
|
756
|
+
* persisted copy, so only records changed since are read again.
|
|
646
757
|
* `runningSessionIds` may be a function, asked once the reads are done. */
|
|
647
758
|
export async function listStoredAgentWorkersAsync({ runningSessionIds = null } = {}) {
|
|
648
759
|
const agentIndexPath = storedAgentWorkerIndexPath();
|
|
@@ -650,6 +761,7 @@ export async function listStoredAgentWorkersAsync({ runningSessionIds = null } =
|
|
|
650
761
|
const [agentIndex, leadIndex] = await Promise.all([
|
|
651
762
|
readWorkerIndexFileAsync(agentIndexPath),
|
|
652
763
|
readWorkerIndexFileAsync(leadIndexPath),
|
|
764
|
+
seedPersistedHeaders(),
|
|
653
765
|
]);
|
|
654
766
|
const headers = new Map();
|
|
655
767
|
const childRows = workerRows(agentIndex);
|
|
@@ -683,6 +795,7 @@ export async function listStoredAgentWorkersAsync({ runningSessionIds = null } =
|
|
|
683
795
|
passHeaders = null;
|
|
684
796
|
passIndexes = null;
|
|
685
797
|
forgetUnreadHeaders();
|
|
798
|
+
schedulePersistHeaders();
|
|
686
799
|
}
|
|
687
800
|
}
|
|
688
801
|
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
// Tail reminders: short per-model/per-language nudges appended to the very end
|
|
2
2
|
// of an outgoing request (after the last tool result), fresh on every request.
|
|
3
|
-
// They are never stored
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// They are never stored as messages of their own; the Anthropic builders place
|
|
4
|
+
// their last cache breakpoint BEFORE them. The assistant turn a request produces
|
|
5
|
+
// keeps that request's reminder text (`turnReminder`, send-with-recovery.mjs).
|
|
6
|
+
// How each provider places them (providers/lib/tail-reminder-split.mjs):
|
|
7
|
+
// - legacy: the reminder ends this request only and is gone from the next;
|
|
8
|
+
// - turn-scoped (first-party Anthropic on a supported model, opt-in per
|
|
9
|
+
// provider): a clear_at system message that every later request re-sends
|
|
10
|
+
// verbatim before its assistant turn, so the wire is append-only and the
|
|
11
|
+
// model's earlier thinking stays bound to an unchanged prefix.
|
|
6
12
|
//
|
|
7
13
|
// HOW TO ADD A RULE (a new per-model/per-language fix needs nothing else):
|
|
8
14
|
// 1. Append {id, when, text} to TAIL_REMINDER_RULES. `when` holds any of
|
|
@@ -17,7 +23,7 @@ import { AGENT_MODE } from './agent-mode.mjs';
|
|
|
17
23
|
import { _buildLeadLanguage } from './manager/rules-cache.mjs';
|
|
18
24
|
import { TAIL_REMINDER_META } from '../providers/lib/tail-reminder-split.mjs';
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
const TAIL_REMINDER_TAGS = Object.freeze(['provider', 'model', 'language', 'role']);
|
|
21
27
|
const ROLES = ['lead', 'bot', 'worker'];
|
|
22
28
|
|
|
23
29
|
const PROVIDER_FAMILIES = Object.freeze({
|
|
@@ -84,7 +90,7 @@ export function modelFamilyId(model) {
|
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/** The session's role: 'worker' (agent-owned), 'bot' (agent mode) or 'lead'. */
|
|
87
|
-
|
|
93
|
+
function tailReminderRole(session) {
|
|
88
94
|
if (session?.owner === 'agent') return 'worker';
|
|
89
95
|
return session?.sessionMode === AGENT_MODE ? 'bot' : 'lead';
|
|
90
96
|
}
|
|
@@ -33,6 +33,10 @@ export function createBatchState(ctx) {
|
|
|
33
33
|
pushToolResultMessage: ctx.pushToolResultMessage,
|
|
34
34
|
throwIfAborted: ctx.throwIfAborted,
|
|
35
35
|
repeatFailLimit: ctx.repeatFailLimit,
|
|
36
|
+
// Calls of this batch a capped turn may still run (session/loop/turn-limits.mjs).
|
|
37
|
+
callsAllowed: Number.isFinite(ctx.callsAllowed) ? ctx.callsAllowed : Infinity,
|
|
38
|
+
// After this time (epoch ms) no call of the batch starts.
|
|
39
|
+
wrapUpAt: Number.isFinite(ctx.wrapUpAt) ? ctx.wrapUpAt : Infinity,
|
|
36
40
|
toolBatchId: ctx.toolBatchId,
|
|
37
41
|
executeToolFn: typeof ctx.executeToolFn === 'function' ? ctx.executeToolFn : executeTool,
|
|
38
42
|
dedupStubTotal: ctx.dedupStubTotal,
|
|
@@ -12,6 +12,7 @@ import { executeBatchCall } from './tool-batch/execute-call.mjs';
|
|
|
12
12
|
import { recordCallOutcome } from './tool-batch/record-outcome.mjs';
|
|
13
13
|
import { finalizeBatchResults } from './tool-batch/finalize.mjs';
|
|
14
14
|
import { flushBatch } from './tool-batch/flush.mjs';
|
|
15
|
+
import { turnLimitRefusal } from './loop/turn-limits.mjs';
|
|
15
16
|
|
|
16
17
|
export async function processToolBatch(ctx) {
|
|
17
18
|
const batch = createBatchState(ctx);
|
|
@@ -19,6 +20,10 @@ export async function processToolBatch(ctx) {
|
|
|
19
20
|
for (let callIndex = 0; callIndex < calls.length; callIndex += 1) {
|
|
20
21
|
const call = calls[callIndex];
|
|
21
22
|
if (isBuiltinTool(call.name)) call.name = canonicalizeBuiltinToolName(call.name);
|
|
23
|
+
if (callIndex >= batch.callsAllowed || Date.now() >= batch.wrapUpAt) {
|
|
24
|
+
stageToolResult(batch, turnLimitRefusal(call));
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
22
27
|
const { skip, ctSig, sigs } = preDispatchSkip(batch, call);
|
|
23
28
|
if (skip !== null) {
|
|
24
29
|
stageToolResult(batch, skip);
|