negotium 0.22.2 → 0.22.3

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/registry.js CHANGED
@@ -132,7 +132,7 @@ var codexRegistryOperations = {
132
132
  return { sessionId: threadId, rolloutPath };
133
133
  },
134
134
  async forkSession({ parentSessionId }) {
135
- const { forkCodexSession } = await import("./chunk-gzph93xn.js");
135
+ const { forkCodexSession } = await import("./chunk-6c637pwe.js");
136
136
  return await forkCodexSession(parentSessionId);
137
137
  },
138
138
  async cleanupRollouts({ sessionIds }) {
@@ -23,7 +23,7 @@ import { deliverPeerReply, type RemoteReplyRoute } from "#mcp/session-comm/peer-
23
23
  import { type CoalescingRunner, createCoalescingRunner } from "#outbox/coalescing-runner";
24
24
  import { deleteProcessingFile, drainOutboxFile, parseOutboxLine } from "#outbox/file-ops";
25
25
  import { watchDir } from "#outbox/utils";
26
- import { resolveTopicWorkspaceDir, SESSION_INBOX_DIR } from "#platform/config";
26
+ import { SESSION_INBOX_DIR } from "#platform/config";
27
27
  import { FROM_AUTO_CONTINUE, FROM_SELF_SCHEDULE } from "#platform/constants";
28
28
  import { appendJsonlEntry, readJsonlLines } from "#platform/jsonl";
29
29
  import { logger } from "#platform/logger";
@@ -123,16 +123,22 @@ export function normalizeAskTarget(target: unknown): "main" | "cron" | null {
123
123
  }
124
124
 
125
125
  /**
126
- * Main-session asks run as a normal turn on the target topic's own session
126
+ * Every ask now runs as a normal turn on the target's own session
127
127
  * (tell_session-style, visible in the target) instead of on a fork. Codex
128
128
  * scopes its prompt cache to the thread, so a fork's first request never
129
129
  * reused the parent's cache and every ask paid full input price; running on
130
- * the target session keeps the cache warm and gives every agent the same
131
- * ask semantics. Cron asks keep the fork path so they never mutate the shared
132
- * cron session.
130
+ * the target session keeps the cache warm.
131
+ *
132
+ * Cron asks (2026-10-20) dropped the fork too, trading it for
133
+ * `turnConcurrency: "isolated"` so the ask doesn't queue behind the topic's
134
+ * main visible turn. This does NOT serialize against a scheduled cron run
135
+ * landing on the identical session at the same moment — the fork used to
136
+ * sidestep that by mutating a private copy instead of the shared session.
137
+ * Treat a corrupted/interleaved cron rollout as a known residual risk until
138
+ * the cron scheduler itself gets a per-session lock.
133
139
  */
134
- export function askRunsOnTargetSession(target: "main" | "cron"): boolean {
135
- return target === "main";
140
+ export function askRunsOnTargetSession(_target: "main" | "cron"): boolean {
141
+ return true;
136
142
  }
137
143
 
138
144
  export function resolveAskParentSessionId(
@@ -1070,33 +1076,16 @@ async function handleAskEntry(
1070
1076
  const { getTopic } = await import("#storage/api-topics");
1071
1077
  const fullTopic = getTopic(topic.id);
1072
1078
  const agentOverride = (fullTopic?.agent ?? topic.agent ?? undefined) as AgentKind | undefined;
1073
- const cwd = resolveTopicWorkspaceDir(topic.id);
1074
1079
  if (!agentOverride) {
1075
1080
  clearPendingAskForEntry(scope, entry, topicName, "target-agent-missing");
1076
1081
  await notifyAskDrop(scope, entry, topicName, "(error: target topic has no agent)");
1077
1082
  return;
1078
1083
  }
1079
1084
 
1080
- // Fork immediately — snapshot the target topic's state at the moment the
1081
- // inbox entry is consumed (just like clawgram). The ask session reads the
1082
- // history that exists right now, not after any in-flight user turn finishes.
1083
- const { cleanupAgentFork, forkAgentSession } = await import("#agents/fork");
1084
- const { getRegistry, getRegistryOperations } = await import("#agents/registry");
1085
- const { resolveModelForAgent } = await import("#agents/model-catalog");
1086
- const { getApiTopicConfig } = await import("#storage/api-topic-config");
1087
- const { readConversation } = await import("#storage/conversations");
1088
- const registry = getRegistry(agentOverride);
1089
- const topicConfig = getApiTopicConfig(topic.id);
1090
- const model = resolveModelForAgent(
1091
- agentOverride,
1092
- topicConfig?.model ?? fullTopic?.defaultModel,
1093
- registry,
1094
- );
1095
- const requestedEffort = topicConfig?.effort ?? fullTopic?.defaultEffort;
1096
- const effort =
1097
- requestedEffort && registry.validateEffort(requestedEffort)
1098
- ? requestedEffort
1099
- : registry.defaultEffort;
1085
+ // Every ask runs as a normal turn on the target's own session (see
1086
+ // askRunsOnTargetSession above), so no fork/model/effort resolution is
1087
+ // needed here — triggerTopicAiTurn below resolves those itself, same as
1088
+ // a regular tell_session.
1100
1089
  const cronSession =
1101
1090
  askTarget === "cron" ? getRegisteredCronSession(topic.id, agentOverride) : null;
1102
1091
  const parentSessionId =
@@ -1161,6 +1150,17 @@ async function handleAskEntry(
1161
1150
  label: `Ask from ${fromLabel}`,
1162
1151
  message: entry.message,
1163
1152
  },
1153
+ // cron: target the cron session explicitly and run it isolated from
1154
+ // the topic's main visible turn (no fork — see askRunsOnTargetSession's
1155
+ // doc comment for the residual concurrency tradeoff). main: default to
1156
+ // the topic's own session/turn slot, same as tell_session.
1157
+ ...(askTarget === "cron"
1158
+ ? {
1159
+ sessionId: parentSessionId,
1160
+ sessionScope: "isolated" as const,
1161
+ turnConcurrency: "isolated" as const,
1162
+ }
1163
+ : {}),
1164
1164
  onSettled,
1165
1165
  onDispatched,
1166
1166
  });
@@ -1171,149 +1171,11 @@ async function handleAskEntry(
1171
1171
  return;
1172
1172
  }
1173
1173
 
1174
- const forkNative = parentSessionId
1175
- ? () =>
1176
- forkAgentSession({
1177
- agent: agentOverride,
1178
- parentSessionId,
1179
- cwd,
1180
- userId: scope.userId,
1181
- topicName,
1182
- title: `ask: ${entry.from} -> ${targetLabel}`,
1183
- model,
1184
- ...(effort ? { effort } : {}),
1185
- })
1186
- : undefined;
1187
-
1188
- let forkPlan: AskForkPlan;
1189
- try {
1190
- forkPlan = await createAskForkPlan({
1191
- // Read once: queued/replayed asks must keep the history visible at inbox
1192
- // consumption time even if the target topic advances while they wait.
1193
- entries: askTarget === "cron" ? [] : readConversation(scope.userId, topicName),
1194
- ...(forkNative ? { forkNative } : {}),
1195
- ...(askTarget === "cron" && forkNative ? { prepareSession: forkNative } : {}),
1196
- synthesize: async (entries) => {
1197
- if (askTarget === "cron") {
1198
- throw new Error("cron session fork failed");
1199
- }
1200
- const rollout = getRegistryOperations(agentOverride).writeRollout({
1201
- cwd,
1202
- entries,
1203
- model,
1204
- ...(effort ? { effort } : {}),
1205
- });
1206
- return {
1207
- agent: agentOverride,
1208
- forkId: rollout.sessionId,
1209
- rolloutPath: rollout.rolloutPath,
1210
- };
1211
- },
1212
- onNativeForkError: (err) => {
1213
- logger.warn(
1214
- { err, topicName, from: entry.from, requestId, parentSessionId },
1215
- "session-inbox: native target fork failed",
1216
- );
1217
- },
1218
- });
1219
- } catch (err) {
1220
- // This entry has already been claimed. Convert preparation failure into a
1221
- // terminal ask result so processTopicInbox can delete its `.processing`
1222
- // file instead of leaving the request undiscoverable until another write.
1223
- logger.error(
1224
- { err, topicName, from: entry.from, requestId, parentSessionId },
1225
- "session-inbox: failed to prepare target ask session",
1226
- );
1227
- try {
1228
- clearPendingAskForEntry(scope, entry, topicName, "target-session-prepare-failed");
1229
- } catch (clearError) {
1230
- logger.warn(
1231
- { err: clearError, topicName, from: entry.from, requestId },
1232
- "session-inbox: failed to clear ask after session preparation failure",
1233
- );
1234
- }
1235
- const message = "(error: target session could not be prepared)";
1236
- if (remoteReply) {
1237
- try {
1238
- const delivered = await deliverPeerReply(remoteReply, targetLabel, message, "error");
1239
- if (!delivered) {
1240
- logger.warn(
1241
- { topicName, from: entry.from, requestId },
1242
- "session-inbox: failed to deliver remote ask preparation error",
1243
- );
1244
- }
1245
- } catch (deliveryError) {
1246
- logger.warn(
1247
- { err: deliveryError, topicName, from: entry.from, requestId },
1248
- "session-inbox: remote ask preparation error delivery threw",
1249
- );
1250
- }
1251
- } else {
1252
- try {
1253
- await notifyAskDrop(scope, entry, targetLabel, message);
1254
- } catch (deliveryError) {
1255
- logger.warn(
1256
- { err: deliveryError, topicName, from: entry.from, requestId },
1257
- "session-inbox: local ask preparation error delivery threw",
1258
- );
1259
- }
1260
- }
1261
- return;
1262
- }
1263
-
1264
- if (askTarget === "cron") {
1265
- const currentCronSession = getRegisteredCronSession(topic.id, agentOverride);
1266
- if (
1267
- !cronSession ||
1268
- !currentCronSession ||
1269
- currentCronSession.sessionId !== cronSession.sessionId ||
1270
- currentCronSession.ownerUserId !== cronSession.ownerUserId
1271
- ) {
1272
- cleanupAgentFork(forkPlan.forkHandle);
1273
- clearPendingAskForEntry(scope, entry, topicName, "cron-session-changed-during-fork");
1274
- const message =
1275
- "(error: cron session changed while preparing the request; retry the request)";
1276
- if (remoteReply) {
1277
- await deliverPeerReply(remoteReply, targetLabel, message, "error");
1278
- } else {
1279
- await notifyAskDrop(scope, entry, targetLabel, message);
1280
- }
1281
- return;
1282
- }
1283
- }
1284
-
1285
- // Trigger the AI turn immediately with the pre-forked session.
1286
- triggerTopicAiTurn(
1287
- topic.id,
1288
- String(scope.userId),
1289
- prompt,
1290
- (agentOverride ?? topic.agent ?? undefined) as Parameters<typeof triggerTopicAiTurn>[3],
1291
- {
1292
- origin: entry.from,
1293
- requestId,
1294
- contextId: entry.contextId,
1295
- depth: (entry.fromDepth ?? 0) + 1,
1296
- silent: true,
1297
- sessionId: forkPlan.forkHandle.forkId,
1298
- // The fork is read-only and session-isolated, so it does not need the
1299
- // target topic's visible execution slot. Answer immediately even while
1300
- // that topic is processing a user turn.
1301
- turnConcurrency: "isolated",
1302
- forkHandle: forkPlan.forkHandle,
1303
- // Keep a regeneration recipe even though the first fork is eager. The
1304
- // isolated room prevents user preemption; the recipe remains available
1305
- // for a clean retry if the provider reports an expired session.
1306
- prepareSession: forkPlan.prepareSession,
1307
- cwd,
1308
- onSettled,
1309
- onDispatched,
1310
- },
1311
- );
1312
-
1313
- logger.info(
1314
- { topicName, from: entry.from, requestId, callerTopicId },
1315
- "session-inbox: ask delivered",
1316
- );
1174
+ // Every ask now runs on the target's own session (see
1175
+ // askRunsOnTargetSession above) — the fork-and-copy path this used to
1176
+ // fall through to for ask_cron was removed 2026-10-20. createAskForkPlan
1177
+ // itself is kept (still exported + unit-tested) in case a future caller
1178
+ // needs a true read-only copy again.
1317
1179
  }
1318
1180
 
1319
1181
  // ── Bootstrap ───────────────────────────────────────────────────────
@@ -1 +1 @@
1
- export const NEGOTIUM_VERSION = "0.22.2";
1
+ export const NEGOTIUM_VERSION = "0.22.3";
@@ -1 +1 @@
1
- export declare const NEGOTIUM_VERSION = "0.22.2";
1
+ export declare const NEGOTIUM_VERSION = "0.22.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "negotium",
3
- "version": "0.22.2",
3
+ "version": "0.22.3",
4
4
  "type": "module",
5
5
  "description": "Install the Negotium multi-agent runtime and CLI with one package",
6
6
  "license": "Apache-2.0",