negotium 0.2.3 → 0.2.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/dist/agent-helpers.js +3366 -3048
- package/dist/agent-helpers.js.map +49 -48
- package/dist/background-bash.js +1 -3
- package/dist/background-bash.js.map +4 -4
- package/dist/browser-runtime.js +137 -23
- package/dist/browser-runtime.js.map +11 -10
- package/dist/chunk-qmbcbhyy.js +2407 -0
- package/dist/chunk-qmbcbhyy.js.map +35 -0
- package/dist/hosted-agent.js +56 -39
- package/dist/hosted-agent.js.map +13 -13
- package/dist/main.js +3276 -2808
- package/dist/main.js.map +54 -51
- package/dist/mcp-factories.js +6471 -6148
- package/dist/mcp-factories.js.map +49 -48
- package/dist/prompts.js +1 -3
- package/dist/prompts.js.map +4 -4
- package/dist/query-runtime.js +22 -4
- package/dist/query-runtime.js.map +6 -6
- package/dist/registry.js +169 -213
- package/dist/registry.js.map +8 -9
- package/dist/rollout.js +1 -1
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +6 -12
- package/dist/runtime/src/agents/claude-provider.ts +2 -2
- package/dist/runtime/src/agents/codex-app-server.ts +136 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +1 -1
- package/dist/runtime/src/agents/codex-provider.ts +22 -11
- package/dist/runtime/src/agents/codex-registry.ts +9 -17
- package/dist/runtime/src/agents/contracts.ts +2 -0
- package/dist/runtime/src/agents/execution-host.ts +6 -0
- package/dist/runtime/src/agents/fork.ts +24 -18
- package/dist/runtime/src/agents/idle-archiver.ts +7 -2
- package/dist/runtime/src/agents/index.ts +21 -15
- package/dist/runtime/src/agents/maestro-provider.ts +1 -1
- package/dist/runtime/src/agents/maestro-registry.ts +26 -7
- package/dist/runtime/src/agents/memory-archive-policy.ts +20 -0
- package/dist/runtime/src/agents/rollout/codex.ts +31 -23
- package/dist/runtime/src/agents/self-config-core.ts +2 -4
- package/dist/runtime/src/agents/topic-agent-switch.ts +4 -10
- package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +77 -19
- package/dist/runtime/src/application/switch-topic-model.ts +7 -10
- package/dist/runtime/src/index.ts +4 -0
- package/dist/runtime/src/mcp/factories/session-comm.ts +12 -0
- package/dist/runtime/src/mcp/session-comm/default-host.ts +16 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +25 -0
- package/dist/runtime/src/mcp/wiki-server.ts +19 -12
- package/dist/runtime/src/platform/config.ts +4 -3
- package/dist/runtime/src/platform/log-rotation.ts +45 -0
- package/dist/runtime/src/platform/mcp-config.ts +9 -3
- package/dist/runtime/src/platform/playwright/manager.ts +57 -1
- package/dist/runtime/src/platform/playwright/profile-management.ts +33 -0
- package/dist/runtime/src/platform/playwright/public-runtime.ts +5 -0
- package/dist/runtime/src/platform/playwright/vault-broker.ts +9 -10
- package/dist/runtime/src/query/active-rooms.ts +7 -1
- package/dist/runtime/src/runtime/errors.ts +1 -0
- package/dist/runtime/src/runtime/turn-event-stream.ts +46 -1
- package/dist/runtime/src/runtime/turn-runner.ts +176 -100
- package/dist/runtime/src/runtime/user-turn-envelope.ts +19 -0
- package/dist/runtime/src/storage/browser-profiles.ts +35 -10
- package/dist/runtime/src/storage/runtime-gateway-submissions.ts +12 -3
- package/dist/runtime/src/storage/runtime-turn-requests.ts +68 -7
- package/dist/runtime/src/storage/storage-host.ts +31 -5
- package/dist/runtime/src/storage/vault.ts +30 -2
- package/dist/runtime/src/topics/derive.ts +38 -2
- package/dist/runtime/src/topics/lifecycle.ts +12 -2
- package/dist/runtime/src/types.ts +2 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js +22 -4
- package/dist/runtime-helpers.js.map +5 -5
- package/dist/storage.js +22 -2
- package/dist/storage.js.map +5 -5
- package/dist/types/packages/core/src/agents/api-topic-agent-switch.d.ts +1 -1
- package/dist/types/packages/core/src/agents/codex-app-server.d.ts +13 -0
- package/dist/types/packages/core/src/agents/codex-native-multi-agent.d.ts +1 -0
- package/dist/types/packages/core/src/agents/contracts.d.ts +2 -0
- package/dist/types/packages/core/src/agents/execution-host.d.ts +2 -0
- package/dist/types/packages/core/src/agents/fork.d.ts +8 -7
- package/dist/types/packages/core/src/agents/memory-archive-policy.d.ts +10 -0
- package/dist/types/packages/core/src/agents/rollout/codex.d.ts +4 -2
- package/dist/types/packages/core/src/agents/self-config-core.d.ts +1 -1
- package/dist/types/packages/core/src/mcp/factories/session-comm.d.ts +1 -0
- package/dist/types/packages/core/src/platform/config.d.ts +0 -1
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +3 -0
- package/dist/types/packages/core/src/platform/playwright/manager.d.ts +15 -0
- package/dist/types/packages/core/src/platform/playwright/profile-management.d.ts +8 -0
- package/dist/types/packages/core/src/platform/playwright/public-runtime.d.ts +2 -1
- package/dist/types/packages/core/src/query/active-rooms.d.ts +7 -1
- package/dist/types/packages/core/src/runtime/turn-runner.d.ts +42 -1
- package/dist/types/packages/core/src/runtime/user-turn-envelope.d.ts +7 -0
- package/dist/types/packages/core/src/storage/browser-profiles.d.ts +3 -1
- package/dist/types/packages/core/src/storage/runtime-turn-requests.d.ts +17 -0
- package/dist/types/packages/core/src/storage/storage-host.d.ts +1 -0
- package/dist/types/packages/core/src/storage/vault.d.ts +8 -0
- package/dist/types/packages/core/src/types.d.ts +2 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js +19 -5
- package/dist/vault.js.map +5 -5
- package/package.json +2 -2
- package/dist/chunk-hsqgb2hw.js +0 -1223
- package/dist/chunk-hsqgb2hw.js.map +0 -19
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import type { ForkRegistryResult } from "#agents/contracts";
|
|
2
3
|
import { getRegistryOperations } from "#agents/registry";
|
|
3
4
|
import { logger } from "#platform/logger";
|
|
4
5
|
import type { AgentKind, EffortLevel } from "#types";
|
|
@@ -7,9 +8,10 @@ import type { AgentKind, EffortLevel } from "#types";
|
|
|
7
8
|
* Agent-aware fork primitives.
|
|
8
9
|
*
|
|
9
10
|
* Strategy lives in each agent's registry (`*-registry.ts:forkSession`).
|
|
10
|
-
* Claude uses the SDK's native `forkSession
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Claude uses the SDK's native `forkSession`, Codex uses App Server's native
|
|
12
|
+
* `thread/fork`, and Maestro uses the SDK's `forkSessionAt`. Providers without
|
|
13
|
+
* a native branch primitive synthesize from the provider-agnostic conversation
|
|
14
|
+
* log. All surfaces converge on ForkHandle.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
export interface ForkAgentSessionOptions {
|
|
@@ -35,13 +37,15 @@ export interface ForkHandle {
|
|
|
35
37
|
forkId: string;
|
|
36
38
|
/** Absolute path of the rollout file to remove on cleanup. */
|
|
37
39
|
rolloutPath: string;
|
|
40
|
+
/** Additional provider-owned files removed with the primary rollout. */
|
|
41
|
+
cleanupPaths?: string[];
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export interface AgentForkHost {
|
|
41
45
|
forkSession(
|
|
42
46
|
agent: AgentKind,
|
|
43
47
|
options: Omit<ForkAgentSessionOptions, "agent">,
|
|
44
|
-
): Promise<
|
|
48
|
+
): Promise<ForkRegistryResult>;
|
|
45
49
|
exists(path: string): boolean;
|
|
46
50
|
unlink(path: string): void;
|
|
47
51
|
warn(details: Record<string, unknown>, message: string): void;
|
|
@@ -56,22 +60,24 @@ export function createAgentForkHelpers(host: AgentForkHost): AgentForkHelpers {
|
|
|
56
60
|
return {
|
|
57
61
|
async forkAgentSession(options) {
|
|
58
62
|
const { agent, ...forkOptions } = options;
|
|
59
|
-
const { forkId, rolloutPath } = await host.forkSession(agent, forkOptions);
|
|
60
|
-
return { agent, forkId, rolloutPath };
|
|
63
|
+
const { forkId, rolloutPath, cleanupPaths } = await host.forkSession(agent, forkOptions);
|
|
64
|
+
return { agent, forkId, rolloutPath, ...(cleanupPaths?.length ? { cleanupPaths } : {}) };
|
|
61
65
|
},
|
|
62
66
|
cleanupAgentFork(handle) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
for (const path of [handle.rolloutPath, ...(handle.cleanupPaths ?? [])]) {
|
|
68
|
+
try {
|
|
69
|
+
if (host.exists(path)) host.unlink(path);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
host.warn(
|
|
72
|
+
{
|
|
73
|
+
error,
|
|
74
|
+
agent: handle.agent,
|
|
75
|
+
forkId: handle.forkId,
|
|
76
|
+
path,
|
|
77
|
+
},
|
|
78
|
+
"cleanupAgentFork: failed to remove rollout",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
75
81
|
}
|
|
76
82
|
},
|
|
77
83
|
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { runArchiverTurn } from "#agents/archiver";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
countMemoryArchiveExchanges,
|
|
4
|
+
meetsMemoryArchiveExchangeThreshold,
|
|
5
|
+
} from "#agents/memory-archive-policy";
|
|
3
6
|
import { logger } from "#platform/logger";
|
|
4
7
|
import { getRoomQuery } from "#query/active-rooms";
|
|
5
8
|
import { getMessagesForTopicAfterRowid } from "#storage/api-messages";
|
|
@@ -141,7 +144,9 @@ export function archiveActiveTopicForMemory(
|
|
|
141
144
|
return null;
|
|
142
145
|
}
|
|
143
146
|
const exchangeCount = countMemoryArchiveExchanges(pending);
|
|
144
|
-
skipMemoryTurn =
|
|
147
|
+
skipMemoryTurn =
|
|
148
|
+
options.minExchanges !== undefined &&
|
|
149
|
+
!meetsMemoryArchiveExchangeThreshold(exchangeCount, options.minExchanges, topic);
|
|
145
150
|
const archived = (options.archiveMessages ?? archiveTopicMessages)(topicId, topic.title, {
|
|
146
151
|
afterRowid,
|
|
147
152
|
reason: options.reason,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { hostedCodexHomePath } from "#agents/execution-host";
|
|
4
5
|
import { getRegistryOperations } from "#agents/registry";
|
|
5
6
|
import { encodeClaudeCwd } from "#agents/rollout/claude";
|
|
6
7
|
import { resolveTaskEventScope, withTaskSnapshots } from "#agents/task-events";
|
|
@@ -108,7 +109,7 @@ export async function resolveSessionFileMissing(
|
|
|
108
109
|
return !existsSync(path);
|
|
109
110
|
}
|
|
110
111
|
case "codex": {
|
|
111
|
-
const sessionsDir = join(
|
|
112
|
+
const sessionsDir = join(hostedCodexHomePath(), "sessions");
|
|
112
113
|
const glob = new Bun.Glob(`**/rollout-*-${sessionId}.jsonl`);
|
|
113
114
|
for await (const _rel of glob.scan({ cwd: sessionsDir, onlyFiles: true })) {
|
|
114
115
|
return false; // found at least one match → session exists
|
|
@@ -128,18 +129,17 @@ export async function resolveSessionFileMissing(
|
|
|
128
129
|
|
|
129
130
|
/**
|
|
130
131
|
* Rebuild a missing SDK session file from the unified conversation log, or
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* should start a fresh session (no entries, rebuild failed, etc.).
|
|
132
|
+
* report whether the session is ready, genuinely has no history, or could not
|
|
133
|
+
* be repaired. Repair failures must not silently discard conversation context.
|
|
134
134
|
*/
|
|
135
135
|
async function maybeRebuildSession(
|
|
136
136
|
opts: AgentQueryOptions & { userId: string; session: string },
|
|
137
|
-
): Promise<
|
|
138
|
-
if (!opts.sessionId) return
|
|
137
|
+
): Promise<"ready" | "fresh" | "failed"> {
|
|
138
|
+
if (!opts.sessionId) return "ready"; // fresh session, nothing to rebuild
|
|
139
139
|
|
|
140
140
|
try {
|
|
141
141
|
const missing = await resolveSessionFileMissing(opts.agent, opts.sessionId, opts.cwd);
|
|
142
|
-
if (!missing) return
|
|
142
|
+
if (!missing) return "ready"; // file still on disk
|
|
143
143
|
|
|
144
144
|
logger.info(
|
|
145
145
|
{ agent: opts.agent, sessionId: opts.sessionId, topic: opts.session },
|
|
@@ -152,7 +152,7 @@ async function maybeRebuildSession(
|
|
|
152
152
|
{ agent: opts.agent, sessionId: opts.sessionId },
|
|
153
153
|
"no unified log entries to rebuild — starting fresh session",
|
|
154
154
|
);
|
|
155
|
-
return
|
|
155
|
+
return "fresh";
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
const operations = getRegistryOperations(opts.agent);
|
|
@@ -167,13 +167,13 @@ async function maybeRebuildSession(
|
|
|
167
167
|
{ agent: opts.agent, sessionId: result.sessionId, rolloutPath: result.rolloutPath },
|
|
168
168
|
"session rebuilt from unified log",
|
|
169
169
|
);
|
|
170
|
-
return
|
|
170
|
+
return "ready";
|
|
171
171
|
} catch (err) {
|
|
172
172
|
logger.warn(
|
|
173
173
|
{ err, agent: opts.agent, sessionId: opts.sessionId },
|
|
174
|
-
"session rebuild failed —
|
|
174
|
+
"session rebuild failed — preserving resume key",
|
|
175
175
|
);
|
|
176
|
-
return
|
|
176
|
+
return "failed";
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|
|
@@ -182,11 +182,17 @@ export async function* runAgent(opts: AgentQueryOptions): AsyncGenerator<Unified
|
|
|
182
182
|
const dispatchOpts: AgentQueryOptions = { ...opts };
|
|
183
183
|
|
|
184
184
|
if (hasSessionRepairContext(dispatchOpts)) {
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
//
|
|
188
|
-
// a fresh session instead of trying to resume a non-existent one.
|
|
185
|
+
const repair = await maybeRebuildSession(dispatchOpts);
|
|
186
|
+
if (repair === "fresh") {
|
|
187
|
+
// No unified history exists, so a new provider session is intentional.
|
|
189
188
|
dispatchOpts.sessionId = undefined;
|
|
189
|
+
} else if (repair === "failed") {
|
|
190
|
+
yield {
|
|
191
|
+
type: "error",
|
|
192
|
+
content:
|
|
193
|
+
"Provider session could not be rebuilt from conversation history; the existing session was preserved for retry.",
|
|
194
|
+
};
|
|
195
|
+
return;
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
|
|
@@ -242,7 +242,7 @@ export function maestroProvider(opts: AgentQueryOptions): AsyncGenerator<Unified
|
|
|
242
242
|
if (opts.agent !== undefined && opts.agent !== "maestro") {
|
|
243
243
|
throw new Error(`maestroProvider: unexpected agent "${opts.agent}", expected "maestro"`);
|
|
244
244
|
}
|
|
245
|
-
const userId = opts.userId ?? "";
|
|
245
|
+
const userId = opts.vaultUserId ?? opts.userId ?? "";
|
|
246
246
|
// The SDK deliberately ships with an empty MCP resolver. Keep this wiring in
|
|
247
247
|
// the provider adapter rather than every host bootstrap so a new Negotium
|
|
248
248
|
// channel cannot accidentally run Maestro with zero runtime/wiki/task MCPs.
|
|
@@ -5,7 +5,6 @@ import { join, resolve } from "node:path";
|
|
|
5
5
|
import type { AgentRegistry, AgentRegistryOperations } from "#agents/contracts";
|
|
6
6
|
import { assertUuidLike, ensureCwdExists, extractChatPairs } from "#agents/rollout/shared";
|
|
7
7
|
import { logger } from "#platform/logger";
|
|
8
|
-
import { readConversation } from "#storage/conversations";
|
|
9
8
|
import { MAESTRO_EFFORT_VALUES } from "#types";
|
|
10
9
|
|
|
11
10
|
const ALIAS_MAP: Record<string, string> = {
|
|
@@ -80,7 +79,7 @@ function writeRollout(options: Parameters<AgentRegistryOperations["writeRollout"
|
|
|
80
79
|
version: 1,
|
|
81
80
|
cwd: options.cwd,
|
|
82
81
|
createdAt: existingCreatedAt(path) ?? new Date().toISOString(),
|
|
83
|
-
sdkVersion: "0.
|
|
82
|
+
sdkVersion: "0.2.0",
|
|
84
83
|
},
|
|
85
84
|
},
|
|
86
85
|
...messages,
|
|
@@ -96,13 +95,33 @@ export const maestroRegistryOperations: AgentRegistryOperations = {
|
|
|
96
95
|
return writeRollout(options);
|
|
97
96
|
},
|
|
98
97
|
async forkSession(options) {
|
|
99
|
-
|
|
98
|
+
// Fork the full raw history while preserving the compacted working view.
|
|
99
|
+
// A compacted parent that cannot copy its active projection is not a usable
|
|
100
|
+
// cache-preserving fork, so let the caller fall back to bounded synthesis.
|
|
101
|
+
const { deleteMaestroSession, forkSessionAt, loadRawMaestroSession } = await import(
|
|
102
|
+
"maestro-agent-sdk"
|
|
103
|
+
);
|
|
104
|
+
const parentMessages = loadRawMaestroSession(options.parentSessionId);
|
|
105
|
+
if (!parentMessages) {
|
|
106
|
+
throw new Error(`Maestro parent session not found: ${options.parentSessionId}`);
|
|
107
|
+
}
|
|
108
|
+
const parentHasActiveProjection = existsSync(maestroActiveSessionPath(options.parentSessionId));
|
|
109
|
+
const fork = forkSessionAt({
|
|
110
|
+
parentSessionId: options.parentSessionId,
|
|
111
|
+
messageIndex: parentMessages.length,
|
|
100
112
|
cwd: options.cwd,
|
|
101
|
-
|
|
102
|
-
...(options.model ? { model: options.model } : {}),
|
|
103
|
-
...(options.effort ? { effort: options.effort } : {}),
|
|
113
|
+
userId: String(options.userId),
|
|
104
114
|
});
|
|
105
|
-
|
|
115
|
+
if (parentHasActiveProjection && !fork.activeProjectionForked) {
|
|
116
|
+
deleteMaestroSession(fork.sessionId);
|
|
117
|
+
throw new Error(`Maestro active projection could not be forked: ${options.parentSessionId}`);
|
|
118
|
+
}
|
|
119
|
+
const activePath = maestroActiveSessionPath(fork.sessionId);
|
|
120
|
+
return {
|
|
121
|
+
forkId: fork.sessionId,
|
|
122
|
+
rolloutPath: fork.rolloutPath,
|
|
123
|
+
...(existsSync(activePath) ? { cleanupPaths: [activePath] } : {}),
|
|
124
|
+
};
|
|
106
125
|
},
|
|
107
126
|
async cleanupRollouts(options) {
|
|
108
127
|
// Keep the SDK off the daemon startup path, but use its canonical cleanup
|
|
@@ -12,6 +12,26 @@ export interface MemoryArchiveMessage {
|
|
|
12
12
|
*/
|
|
13
13
|
export const MIN_MEMORY_ARCHIVE_EXCHANGES = 6;
|
|
14
14
|
|
|
15
|
+
export interface MemoryArchiveTopicContext {
|
|
16
|
+
isSubagent?: boolean;
|
|
17
|
+
memoryTopicId?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Explicit-memory subagents are usually short, focused turns. Persist one
|
|
22
|
+
* completed exchange back to their selected memory topic, while retaining the
|
|
23
|
+
* normal threshold for regular topics and incomplete/failed turns.
|
|
24
|
+
*/
|
|
25
|
+
export function meetsMemoryArchiveExchangeThreshold(
|
|
26
|
+
exchangeCount: number,
|
|
27
|
+
minExchanges: number,
|
|
28
|
+
topic: MemoryArchiveTopicContext,
|
|
29
|
+
): boolean {
|
|
30
|
+
if (exchangeCount <= 0) return false;
|
|
31
|
+
if (topic.isSubagent && topic.memoryTopicId) return true;
|
|
32
|
+
return exchangeCount >= minExchanges;
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
/** Count completed conversational exchanges while ignoring system/tool/card noise. */
|
|
16
36
|
export function countMemoryArchiveExchanges(rows: readonly MemoryArchiveMessage[]): number {
|
|
17
37
|
let waitingForAssistant = false;
|
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
* synthesized `response_item`/`event_msg` pairs per turn.
|
|
11
11
|
*
|
|
12
12
|
* Tool history is folded into assistant text as `[Tool: ...]` annotations
|
|
13
|
-
* (see shared.ts:extractChatPairs)
|
|
14
|
-
*
|
|
13
|
+
* (see shared.ts:extractChatPairs). Native same-agent forks use App Server's
|
|
14
|
+
* `thread/fork`; this encoder remains the fallback and cross-agent bridge,
|
|
15
|
+
* where synthetic tool_use IDs are too fragile to reconstruct structurally.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import { randomBytes } from "node:crypto";
|
|
18
19
|
import { existsSync, readFileSync, realpathSync, statSync, unlinkSync } from "node:fs";
|
|
19
|
-
import { homedir } from "node:os";
|
|
20
20
|
import { basename, dirname, join, resolve } from "node:path";
|
|
21
|
+
import { hostedCodexHomePath } from "#agents/execution-host";
|
|
21
22
|
import {
|
|
22
23
|
assertUuidLike,
|
|
23
24
|
type ChatPair,
|
|
@@ -40,7 +41,7 @@ interface CodexShell {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
function codexSessionsDir(): string {
|
|
43
|
-
return join(
|
|
44
|
+
return join(hostedCodexHomePath(), "sessions");
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
let _shellCache: CodexShell | null = null;
|
|
@@ -156,6 +157,19 @@ export interface CodexPatchPreview {
|
|
|
156
157
|
changes: CodexPatchChangePreview[];
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
function codexRolloutPath(threadId: string, fallback: Date): string {
|
|
161
|
+
const threadCreatedAt = decodeUuidV7Timestamp(threadId);
|
|
162
|
+
const createdAt = threadCreatedAt === null ? fallback : new Date(threadCreatedAt);
|
|
163
|
+
const yyyy = String(createdAt.getFullYear());
|
|
164
|
+
const mm = String(createdAt.getMonth() + 1).padStart(2, "0");
|
|
165
|
+
const dd = String(createdAt.getDate()).padStart(2, "0");
|
|
166
|
+
const hh = String(createdAt.getHours()).padStart(2, "0");
|
|
167
|
+
const min = String(createdAt.getMinutes()).padStart(2, "0");
|
|
168
|
+
const ss = String(createdAt.getSeconds()).padStart(2, "0");
|
|
169
|
+
const dir = join(codexSessionsDir(), yyyy, mm, dd);
|
|
170
|
+
return join(dir, `rollout-${yyyy}-${mm}-${dd}T${hh}-${min}-${ss}-${threadId}.jsonl`);
|
|
171
|
+
}
|
|
172
|
+
|
|
159
173
|
function canonicalFilePath(path: string): string {
|
|
160
174
|
const absolute = resolve(path);
|
|
161
175
|
try {
|
|
@@ -420,7 +434,7 @@ export function migrateCodexRolloutNativeMultiAgentMetadata(threadId: string): b
|
|
|
420
434
|
}
|
|
421
435
|
}
|
|
422
436
|
|
|
423
|
-
function latestCodexRolloutPath(threadId: string): string | undefined {
|
|
437
|
+
export function latestCodexRolloutPath(threadId: string): string | undefined {
|
|
424
438
|
const sessionsDir = codexSessionsDir();
|
|
425
439
|
const candidates: string[] = [];
|
|
426
440
|
const buckets = candidateDateBuckets(threadId);
|
|
@@ -563,22 +577,17 @@ export function writeCodexRollout(opts: CodexRolloutOptions): CodexRolloutResult
|
|
|
563
577
|
payload: { type: "task_complete" },
|
|
564
578
|
});
|
|
565
579
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
const path = join(dir, `rollout-${tsStr}-${threadId}.jsonl`);
|
|
580
|
+
// Codex indexes a thread by the local wall-clock timestamp encoded in its
|
|
581
|
+
// UUIDv7 creation time. A cross-agent round-trip may rewrite the rollout
|
|
582
|
+
// much later, but the path must remain the original canonical path or
|
|
583
|
+
// `thread/resume` rejects it as missing and the provider starts fresh.
|
|
584
|
+
const path = codexRolloutPath(threadId, now);
|
|
572
585
|
|
|
573
586
|
// Round-trip cleanup: when the caller reused a threadId (claude→codex→...
|
|
574
587
|
// or maestro→codex→... cycles via `findLastSessionIdForAgent`), the SDK's
|
|
575
|
-
// original rollout for that threadId still sits at its
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
// the same threadId — `codex resume <tid>` globs `~/.codex/sessions/**`
|
|
579
|
-
// and the SDK has no spec for which match it picks, so behavior becomes
|
|
580
|
-
// environment-/codex-version-dependent. Sweep every prior file for this
|
|
581
|
-
// threadId BEFORE writing the new one so resume always reads our synth.
|
|
588
|
+
// original rollout for that threadId still sits at its canonical path.
|
|
589
|
+
// Sweep every prior file for this threadId BEFORE replacing that path so
|
|
590
|
+
// legacy writers cannot leave ambiguous duplicates behind.
|
|
582
591
|
//
|
|
583
592
|
// Synchronous on purpose: writeCodexRollout's signature is sync (called
|
|
584
593
|
// inside set_agent's DB transaction), and an async sweep would race with
|
|
@@ -671,11 +680,10 @@ function sweepPriorRolloutsFullTree(threadId: string, sessionsDir: string): void
|
|
|
671
680
|
}
|
|
672
681
|
|
|
673
682
|
/**
|
|
674
|
-
* UUIDv7 ids encode their creation time in the first 48 bits.
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
* inclusive range so a Mon-bridge / Tue-rebridge picks up both.
|
|
683
|
+
* UUIDv7 ids encode their creation time in the first 48 bits. Current writers
|
|
684
|
+
* always use that canonical birth-date bucket, but older writers used the
|
|
685
|
+
* rewrite date during cross-agent bridges. Enumerate the inclusive range from
|
|
686
|
+
* birth date to today so cleanup also removes those legacy misplaced files.
|
|
679
687
|
*
|
|
680
688
|
* Returns null when:
|
|
681
689
|
* - threadId isn't a parseable UUIDv7 (atypical fixture / future format)
|
|
@@ -89,7 +89,7 @@ export type SelfConfigAgentSwitchResult =
|
|
|
89
89
|
| {
|
|
90
90
|
ok: true;
|
|
91
91
|
outcome:
|
|
92
|
-
| { kind: "fresh"; agent: AgentKind; reason: "no-history"
|
|
92
|
+
| { kind: "fresh"; agent: AgentKind; reason: "no-history" }
|
|
93
93
|
| {
|
|
94
94
|
kind: "bridged";
|
|
95
95
|
agent: AgentKind;
|
|
@@ -386,9 +386,7 @@ export function createSelfConfigCore(
|
|
|
386
386
|
const sessionNote =
|
|
387
387
|
switched.outcome.kind === "bridged"
|
|
388
388
|
? ` Conversation history was bridged into a ${agent} session (${switched.outcome.bridgedSessionId}).`
|
|
389
|
-
:
|
|
390
|
-
? " Conversation history bridge failed; the next turn starts fresh."
|
|
391
|
-
: " No prior conversation history was found; the next turn starts fresh.";
|
|
389
|
+
: " No prior conversation history was found; the next turn starts fresh.";
|
|
392
390
|
return ok(
|
|
393
391
|
`Agent for this topic set to '${agent}'. Model/effort reset to '${agent}' defaults.${sessionNote}`,
|
|
394
392
|
);
|
|
@@ -39,7 +39,7 @@ export type SwitchAgentOutcome =
|
|
|
39
39
|
| {
|
|
40
40
|
kind: "fresh";
|
|
41
41
|
agent: AgentKind;
|
|
42
|
-
reason: "no-history"
|
|
42
|
+
reason: "no-history";
|
|
43
43
|
}
|
|
44
44
|
| {
|
|
45
45
|
kind: "bridged";
|
|
@@ -128,17 +128,11 @@ export function switchTopicAgent(
|
|
|
128
128
|
} catch (err) {
|
|
129
129
|
logger.warn(
|
|
130
130
|
{ err, userId, topicName, from: oldAgent, to: agent },
|
|
131
|
-
"switchTopicAgent: rollout encoding failed —
|
|
131
|
+
"switchTopicAgent: rollout encoding failed — preserving current agent session",
|
|
132
132
|
);
|
|
133
|
-
const changed = setTopicAgentAndClearSession({
|
|
134
|
-
userId,
|
|
135
|
-
topicName,
|
|
136
|
-
agent,
|
|
137
|
-
});
|
|
138
|
-
if (!changed) return { ok: false, error: `failed to set agent for "${topicName}"` };
|
|
139
133
|
return {
|
|
140
|
-
ok:
|
|
141
|
-
|
|
134
|
+
ok: false,
|
|
135
|
+
error: `failed to preserve conversation while switching agent for "${topicName}"`,
|
|
142
136
|
};
|
|
143
137
|
}
|
|
144
138
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { appendApiMessage, getApiMessage } from "#storage/api-messages";
|
|
3
3
|
import { getTopicSessionId } from "#storage/api-topics";
|
|
4
4
|
import { db } from "#storage/forum-db";
|
|
@@ -8,12 +8,20 @@ import {
|
|
|
8
8
|
type RuntimeGatewaySubmission,
|
|
9
9
|
recordRuntimeGatewaySubmission,
|
|
10
10
|
} from "#storage/runtime-gateway-submissions";
|
|
11
|
-
import {
|
|
11
|
+
import { requestRuntimeTurnAbort } from "#storage/runtime-leases";
|
|
12
|
+
import { getRuntimeTopicEpoch } from "#storage/runtime-topic-state";
|
|
13
|
+
import { mergeRuntimeUserTurnRequest } from "#storage/runtime-turn-requests";
|
|
12
14
|
import type { MessageDto, TopicDto } from "#types/api";
|
|
13
15
|
|
|
14
16
|
export interface SubmitRuntimeGatewayTurnParams {
|
|
15
17
|
topic: TopicDto;
|
|
18
|
+
/** Canonical execution principal, normally `local`. */
|
|
16
19
|
userId: string;
|
|
20
|
+
/** Authenticated upstream author retained independently from execution. */
|
|
21
|
+
actorUserId?: string;
|
|
22
|
+
actorLabel?: string;
|
|
23
|
+
/** Topic owner's credential namespace. */
|
|
24
|
+
vaultUserId?: string;
|
|
17
25
|
text: string;
|
|
18
26
|
clientMessageId: string;
|
|
19
27
|
requestId?: string;
|
|
@@ -25,9 +33,30 @@ export interface SubmitRuntimeGatewayTurnResult extends RuntimeGatewaySubmission
|
|
|
25
33
|
deduplicated: boolean;
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
function duplicateResult(
|
|
36
|
+
function duplicateResult(
|
|
37
|
+
submission: RuntimeGatewaySubmission,
|
|
38
|
+
params: SubmitRuntimeGatewayTurnParams,
|
|
39
|
+
requestId: string,
|
|
40
|
+
actorUserId: string,
|
|
41
|
+
payloadHash: string,
|
|
42
|
+
): SubmitRuntimeGatewayTurnResult {
|
|
29
43
|
const message = getApiMessage(submission.topicId, submission.messageId);
|
|
30
44
|
if (!message) throw new Error("gateway submission references a missing canonical message");
|
|
45
|
+
if (submission.payloadHash && submission.payloadHash !== payloadHash) {
|
|
46
|
+
throw new RuntimeGatewayIdempotencyConflictError();
|
|
47
|
+
}
|
|
48
|
+
if (
|
|
49
|
+
(!submission.payloadHash &&
|
|
50
|
+
(message.authorName !== params.actorLabel ||
|
|
51
|
+
message.authorId !== actorUserId ||
|
|
52
|
+
message.text !== params.text)) ||
|
|
53
|
+
submission.clientMessageId !== params.clientMessageId ||
|
|
54
|
+
submission.requestId !== requestId ||
|
|
55
|
+
submission.topicId !== params.topic.id ||
|
|
56
|
+
submission.userId !== params.userId
|
|
57
|
+
) {
|
|
58
|
+
throw new RuntimeGatewayIdempotencyConflictError();
|
|
59
|
+
}
|
|
31
60
|
return {
|
|
32
61
|
...submission,
|
|
33
62
|
message,
|
|
@@ -35,6 +64,28 @@ function duplicateResult(submission: RuntimeGatewaySubmission): SubmitRuntimeGat
|
|
|
35
64
|
};
|
|
36
65
|
}
|
|
37
66
|
|
|
67
|
+
function gatewayPayloadHash(
|
|
68
|
+
params: SubmitRuntimeGatewayTurnParams,
|
|
69
|
+
requestId: string,
|
|
70
|
+
actorUserId: string,
|
|
71
|
+
): string {
|
|
72
|
+
return createHash("sha256")
|
|
73
|
+
.update(
|
|
74
|
+
JSON.stringify([
|
|
75
|
+
params.topic.id,
|
|
76
|
+
params.userId,
|
|
77
|
+
actorUserId,
|
|
78
|
+
params.actorLabel ?? null,
|
|
79
|
+
params.vaultUserId ?? null,
|
|
80
|
+
params.text,
|
|
81
|
+
params.clientMessageId,
|
|
82
|
+
requestId,
|
|
83
|
+
params.allowAutoContinue ?? true,
|
|
84
|
+
]),
|
|
85
|
+
)
|
|
86
|
+
.digest("hex");
|
|
87
|
+
}
|
|
88
|
+
|
|
38
89
|
/**
|
|
39
90
|
* Durable ingress for an authenticated external gateway. It deliberately
|
|
40
91
|
* reuses the canonical message table and existing durable turn worker rather
|
|
@@ -45,8 +96,7 @@ function duplicateResult(submission: RuntimeGatewaySubmission): SubmitRuntimeGat
|
|
|
45
96
|
*
|
|
46
97
|
* A distinct type rather than a plain `Error` because the control plane maps
|
|
47
98
|
* this to 409, and it must not be confused with a *missing* `clientMessageId`,
|
|
48
|
-
* which is a malformed request (400).
|
|
49
|
-
* two.
|
|
99
|
+
* which is a malformed request (400).
|
|
50
100
|
*/
|
|
51
101
|
export class RuntimeGatewayIdempotencyConflictError extends Error {
|
|
52
102
|
constructor(message = "clientMessageId or requestId is already bound to another turn") {
|
|
@@ -59,24 +109,19 @@ export function submitRuntimeGatewayTurn(
|
|
|
59
109
|
params: SubmitRuntimeGatewayTurnParams,
|
|
60
110
|
): SubmitRuntimeGatewayTurnResult {
|
|
61
111
|
const requestId = params.requestId ?? params.clientMessageId;
|
|
112
|
+
const actorUserId = params.actorUserId ?? params.userId;
|
|
113
|
+
const payloadHash = gatewayPayloadHash(params, requestId, actorUserId);
|
|
62
114
|
const existing = findRuntimeGatewaySubmission(params.clientMessageId, requestId);
|
|
63
115
|
if (existing) {
|
|
64
|
-
|
|
65
|
-
existing.clientMessageId !== params.clientMessageId ||
|
|
66
|
-
existing.requestId !== requestId ||
|
|
67
|
-
existing.topicId !== params.topic.id ||
|
|
68
|
-
existing.userId !== params.userId
|
|
69
|
-
) {
|
|
70
|
-
throw new RuntimeGatewayIdempotencyConflictError();
|
|
71
|
-
}
|
|
72
|
-
return duplicateResult(existing);
|
|
116
|
+
return duplicateResult(existing, params, requestId, actorUserId, payloadHash);
|
|
73
117
|
}
|
|
74
118
|
|
|
75
119
|
const createdAt = new Date().toISOString();
|
|
76
120
|
const message: MessageDto = {
|
|
77
121
|
id: randomUUID(),
|
|
78
122
|
topicId: params.topic.id,
|
|
79
|
-
authorId:
|
|
123
|
+
authorId: actorUserId,
|
|
124
|
+
authorName: params.actorLabel,
|
|
80
125
|
sourceAdapter: "runtime-gateway",
|
|
81
126
|
sourceMessageId: params.clientMessageId,
|
|
82
127
|
text: params.text,
|
|
@@ -91,23 +136,31 @@ export function submitRuntimeGatewayTurn(
|
|
|
91
136
|
createdAt,
|
|
92
137
|
ackCursor: 0,
|
|
93
138
|
messageCursor: 0,
|
|
139
|
+
payloadHash,
|
|
94
140
|
};
|
|
95
141
|
|
|
96
142
|
try {
|
|
97
143
|
db.transaction(() => {
|
|
98
144
|
appendApiMessage(message, { notify: false });
|
|
99
|
-
|
|
145
|
+
mergeRuntimeUserTurnRequest({
|
|
100
146
|
topicId: params.topic.id,
|
|
101
147
|
userId: params.userId,
|
|
102
|
-
|
|
148
|
+
userMessages: [
|
|
149
|
+
{
|
|
150
|
+
prompt: params.text,
|
|
151
|
+
actorUserId,
|
|
152
|
+
...(params.actorLabel ? { actorLabel: params.actorLabel } : {}),
|
|
153
|
+
},
|
|
154
|
+
],
|
|
103
155
|
allowAutoContinue: params.allowAutoContinue ?? true,
|
|
104
156
|
requestId,
|
|
105
|
-
|
|
157
|
+
topicEpoch: getRuntimeTopicEpoch(params.topic.id),
|
|
106
158
|
execution: {
|
|
107
159
|
sessionId: getTopicSessionId(params.topic.id),
|
|
108
160
|
sessionIdSpecified: true,
|
|
109
161
|
conversationPrompts: [params.text],
|
|
110
162
|
loggedUserMessageCount: 0,
|
|
163
|
+
vaultUserId: params.vaultUserId,
|
|
111
164
|
},
|
|
112
165
|
});
|
|
113
166
|
const acceptedEvent = appendRuntimeEvent("runtime-gateway-ingress", {
|
|
@@ -131,9 +184,14 @@ export function submitRuntimeGatewayTurn(
|
|
|
131
184
|
})();
|
|
132
185
|
} catch {
|
|
133
186
|
const raced = findRuntimeGatewaySubmission(params.clientMessageId, requestId);
|
|
134
|
-
if (raced) return duplicateResult(raced);
|
|
187
|
+
if (raced) return duplicateResult(raced, params, requestId, actorUserId, payloadHash);
|
|
135
188
|
throw new Error("failed to persist gateway turn idempotency record");
|
|
136
189
|
}
|
|
137
190
|
|
|
191
|
+
// A new human message steers the active topic turn. The durable replacement
|
|
192
|
+
// is committed first; the provider observes this abort on its next lease
|
|
193
|
+
// heartbeat and the worker resumes the merged batch after unwind.
|
|
194
|
+
requestRuntimeTurnAbort(params.topic.id, "internal");
|
|
195
|
+
|
|
138
196
|
return { ...submission, message, deduplicated: false };
|
|
139
197
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { switchApiTopicAgent } from "#agents/api-topic-agent-switch";
|
|
2
2
|
import { checkAgentModelAuth } from "#agents/auth-check";
|
|
3
|
-
import {
|
|
4
|
-
import { getRegistry } from "#agents/registry";
|
|
3
|
+
import { selectableModel } from "#agents/model-catalog";
|
|
5
4
|
import { WsHub } from "#bus";
|
|
6
5
|
import { resolveTopicWorkspaceDir } from "#platform/config";
|
|
7
6
|
import { getApiTopicConfig, setApiTopicConfig } from "#storage/api-topic-config";
|
|
8
|
-
import {
|
|
7
|
+
import { getTopic } from "#storage/api-topics";
|
|
9
8
|
|
|
10
9
|
export type SwitchTopicModelResult =
|
|
11
10
|
| { ok: true; model: string; text: string }
|
|
@@ -63,14 +62,12 @@ export function switchTopicModel(params: SwitchTopicModelParams): SwitchTopicMod
|
|
|
63
62
|
});
|
|
64
63
|
if (!switched.ok) return { ok: false, error: switched.error };
|
|
65
64
|
} else {
|
|
66
|
-
const registry = getRegistry(currentAgent);
|
|
67
|
-
const previousModel = resolveModelForAgent(
|
|
68
|
-
currentAgent,
|
|
69
|
-
currentConfig.model ?? topic.defaultModel,
|
|
70
|
-
registry,
|
|
71
|
-
);
|
|
72
65
|
setApiTopicConfig(topic.id, nextConfig);
|
|
73
|
-
|
|
66
|
+
// A provider session is not owned by one model. Codex, Claude, and Maestro
|
|
67
|
+
// all accept the selected model when resuming, so keep the durable resume
|
|
68
|
+
// key and let the next turn continue the same rollout with the new model.
|
|
69
|
+
// Clearing it here silently discarded native tool/history structure and
|
|
70
|
+
// made an ordinary model change look like `/new`.
|
|
74
71
|
}
|
|
75
72
|
|
|
76
73
|
WsHub.get().broadcastTopicUpdated(topic.id);
|
|
@@ -176,6 +176,10 @@ export { FROM_AUTO_CONTINUE } from "#platform/constants";
|
|
|
176
176
|
export { errMsg } from "#platform/error";
|
|
177
177
|
export { appendJsonlEntry } from "#platform/jsonl";
|
|
178
178
|
export { onShutdown, runShutdown } from "#platform/lifecycle";
|
|
179
|
+
export {
|
|
180
|
+
DEFAULT_DAEMON_LOG_ROTATE_BYTES,
|
|
181
|
+
rotateOversizedLog,
|
|
182
|
+
} from "#platform/log-rotation";
|
|
179
183
|
export { logger } from "#platform/logger";
|
|
180
184
|
export type {
|
|
181
185
|
NodeMcpEntry,
|