negotium 0.2.4 → 0.2.6
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 +241 -127
- package/dist/agent-helpers.js.map +19 -19
- package/dist/background-bash.js +1 -3
- package/dist/background-bash.js.map +4 -4
- package/dist/browser-runtime.js +1 -3
- package/dist/browser-runtime.js.map +4 -4
- package/dist/{chunk-1s9ryz8g.js → chunk-qmbcbhyy.js} +20 -12
- package/dist/chunk-qmbcbhyy.js.map +35 -0
- package/dist/hosted-agent.js +13 -10
- package/dist/hosted-agent.js.map +10 -10
- package/dist/main.js +607 -438
- package/dist/main.js.map +30 -29
- package/dist/mcp-factories.js +238 -123
- package/dist/mcp-factories.js.map +18 -18
- package/dist/prompts.js +1 -3
- package/dist/prompts.js.map +4 -4
- package/dist/query-runtime.js +1 -3
- package/dist/query-runtime.js.map +5 -5
- package/dist/registry.js +3 -3
- package/dist/registry.js.map +2 -2
- 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-provider.ts +1 -0
- package/dist/runtime/src/agents/index.ts +19 -14
- package/dist/runtime/src/agents/maestro-provider.ts +1 -1
- package/dist/runtime/src/agents/rollout/codex.ts +25 -18
- 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 +91 -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/wiki-server.ts +19 -12
- package/dist/runtime/src/platform/config.ts +0 -2
- package/dist/runtime/src/platform/log-rotation.ts +45 -0
- package/dist/runtime/src/platform/mcp-config.ts +9 -3
- 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/runtime-gateway-submissions.ts +32 -3
- package/dist/runtime/src/storage/runtime-turn-requests.ts +68 -7
- package/dist/runtime/src/types.ts +2 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js +1 -3
- package/dist/runtime-helpers.js.map +4 -4
- package/dist/storage.js.map +1 -1
- package/dist/types/packages/core/src/agents/api-topic-agent-switch.d.ts +1 -1
- package/dist/types/packages/core/src/agents/self-config-core.d.ts +1 -1
- 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/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/runtime-turn-requests.d.ts +17 -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 +1 -3
- package/dist/vault.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-1s9ryz8g.js.map +0 -35
|
@@ -207,6 +207,8 @@ export function consumePlaywrightUnavailable(userId: string, topic: string | und
|
|
|
207
207
|
|
|
208
208
|
export interface RuntimeMcpBuildContext {
|
|
209
209
|
userId: string;
|
|
210
|
+
/** Vault namespace when credentials belong to a different principal. */
|
|
211
|
+
vaultUserId?: string;
|
|
210
212
|
/** "dm" for DM scope, topic/session name for forum/fork. */
|
|
211
213
|
session: string;
|
|
212
214
|
/** REST/WS topic id when known. Prefer for authorization/scope checks. */
|
|
@@ -527,13 +529,13 @@ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
|
|
|
527
529
|
vault: {
|
|
528
530
|
...commonRuntimeMcpPolicy("vault"),
|
|
529
531
|
build(ctx) {
|
|
530
|
-
const { userId, agent } = ctx;
|
|
532
|
+
const { userId, vaultUserId, agent } = ctx;
|
|
531
533
|
// Normal turns use {{KEY}} directly in tool inputs. Keep the Vault MCP
|
|
532
534
|
// surface focused on key discovery; broker tools remain available from
|
|
533
535
|
// the public factory for compatibility but are no longer the default UX.
|
|
534
|
-
const args = [`--user-id=${userId}`];
|
|
536
|
+
const args = [`--user-id=${vaultUserId ?? userId}`];
|
|
535
537
|
if (agent !== "codex") args.push("--list-only=true");
|
|
536
|
-
return buildBuiltinMcpServer("vault", ctx, () =>
|
|
538
|
+
return buildBuiltinMcpServer("vault", { ...ctx, userId: vaultUserId ?? userId }, () =>
|
|
537
539
|
buildStdioMcpServer(agent, VAULT_SERVER, args),
|
|
538
540
|
);
|
|
539
541
|
},
|
|
@@ -767,6 +769,7 @@ export function getManagerMcpServers(opts: {
|
|
|
767
769
|
*/
|
|
768
770
|
export function getForumMcpServers(opts: {
|
|
769
771
|
userId: string;
|
|
772
|
+
vaultUserId?: string;
|
|
770
773
|
session: string;
|
|
771
774
|
topicId?: string;
|
|
772
775
|
subagentParentTopicId?: string;
|
|
@@ -790,6 +793,7 @@ export function getForumMcpServers(opts: {
|
|
|
790
793
|
}) {
|
|
791
794
|
const {
|
|
792
795
|
userId,
|
|
796
|
+
vaultUserId,
|
|
793
797
|
session,
|
|
794
798
|
topicId,
|
|
795
799
|
subagentParentTopicId,
|
|
@@ -824,6 +828,7 @@ export function getForumMcpServers(opts: {
|
|
|
824
828
|
"forum",
|
|
825
829
|
{
|
|
826
830
|
userId,
|
|
831
|
+
vaultUserId,
|
|
827
832
|
session,
|
|
828
833
|
topicId,
|
|
829
834
|
subagentParentTopicId,
|
|
@@ -945,6 +950,7 @@ export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, u
|
|
|
945
950
|
}
|
|
946
951
|
return getForumMcpServers({
|
|
947
952
|
userId: opts.userId || "local",
|
|
953
|
+
vaultUserId: opts.vaultUserId,
|
|
948
954
|
session: opts.session || "default",
|
|
949
955
|
topicId: opts.topicId,
|
|
950
956
|
subagentParentTopicId: opts.subagentParentTopicId,
|
|
@@ -42,6 +42,8 @@ export interface DeferredInject {
|
|
|
42
42
|
/** Topic execution epoch captured when this work was accepted. */
|
|
43
43
|
runtimeEpoch?: number;
|
|
44
44
|
userId: string;
|
|
45
|
+
/** Credential namespace when the executor and Vault owner differ. */
|
|
46
|
+
vaultUserId?: string;
|
|
45
47
|
prompt: string;
|
|
46
48
|
/** Inject source — the topic name/id this inject came from (never "user"). */
|
|
47
49
|
origin: string;
|
|
@@ -120,8 +122,12 @@ export interface RoomQueryControl {
|
|
|
120
122
|
durableRequestIds?: string[];
|
|
121
123
|
/** Attachment ids currently being processed. User-turn preemption carries them forward. */
|
|
122
124
|
attachments?: string[];
|
|
123
|
-
/**
|
|
125
|
+
/** Latest provider session id observed for this turn. */
|
|
124
126
|
sessionId?: string | null;
|
|
127
|
+
/** True once the provider has published the native session backing this turn. */
|
|
128
|
+
providerSessionObserved?: boolean;
|
|
129
|
+
/** True once provider output proves that this turn's prompt reached the native session. */
|
|
130
|
+
providerTurnContentObserved?: boolean;
|
|
125
131
|
abortController: AbortController;
|
|
126
132
|
abortReason: AbortReason;
|
|
127
133
|
/** Specific failure surfaced when infrastructure aborts the turn. */
|
|
@@ -64,6 +64,7 @@ export function isSessionExpiredError(message: string): boolean {
|
|
|
64
64
|
return (
|
|
65
65
|
message.includes(SESSION_EXPIRED_MSG) ||
|
|
66
66
|
/session was recorded with model .+ but is resuming with/i.test(message) ||
|
|
67
|
+
/failed to resolve rollout path .+file does not exist/i.test(message) ||
|
|
67
68
|
/session.*not found|session.*expired|unknown conversation/i.test(message)
|
|
68
69
|
);
|
|
69
70
|
}
|
|
@@ -64,6 +64,10 @@ import {
|
|
|
64
64
|
updateApiMessageUsage,
|
|
65
65
|
} from "#storage/api-messages";
|
|
66
66
|
import { getTopic, setTopicSessionId } from "#storage/api-topics";
|
|
67
|
+
import {
|
|
68
|
+
getRuntimeUserTurnRequest,
|
|
69
|
+
markRuntimeUserTurnProviderSessionObserved,
|
|
70
|
+
} from "#storage/runtime-turn-requests";
|
|
67
71
|
import { recordUsage } from "#storage/token-stats";
|
|
68
72
|
import type { AgentKind, EffortLevel, PeerRuntimeBridgeContext, UnifiedEvent } from "#types";
|
|
69
73
|
import type { MessageDto } from "#types/api";
|
|
@@ -267,6 +271,30 @@ export async function runTurnEventStream(
|
|
|
267
271
|
for await (const event of events) {
|
|
268
272
|
if (abortController.signal.aborted) break;
|
|
269
273
|
|
|
274
|
+
// A provider may publish its session id before it appends the user
|
|
275
|
+
// prompt to the native rollout. Only semantic provider output proves
|
|
276
|
+
// that the prompt made it into that session. This distinction matters
|
|
277
|
+
// when a second user message preempts the turn in that short window:
|
|
278
|
+
// the replacement must replay both messages instead of dropping the
|
|
279
|
+
// first one merely because a session event was seen.
|
|
280
|
+
if (
|
|
281
|
+
!control.providerTurnContentObserved &&
|
|
282
|
+
(event.type === "text_delta" ||
|
|
283
|
+
event.type === "text" ||
|
|
284
|
+
event.type === "reasoning" ||
|
|
285
|
+
event.type === "tool_use" ||
|
|
286
|
+
event.type === "tool_result" ||
|
|
287
|
+
event.type === "file" ||
|
|
288
|
+
event.type === "result")
|
|
289
|
+
) {
|
|
290
|
+
control.providerTurnContentObserved = true;
|
|
291
|
+
if (control.sessionId) {
|
|
292
|
+
for (const requestId of control.durableRequestIds ?? []) {
|
|
293
|
+
markRuntimeUserTurnProviderSessionObserved(topicId, requestId, control.sessionId);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
270
298
|
switch (event.type) {
|
|
271
299
|
case "text_delta": {
|
|
272
300
|
pendingSawDelta = true;
|
|
@@ -672,6 +700,16 @@ export async function runTurnEventStream(
|
|
|
672
700
|
}
|
|
673
701
|
break;
|
|
674
702
|
case "session":
|
|
703
|
+
// Keep the control synchronized with the provider, not merely the
|
|
704
|
+
// topic row. A superseding user message can then resume the partial
|
|
705
|
+
// native rollout after this process has fully unwound.
|
|
706
|
+
control.sessionId = event.sessionId;
|
|
707
|
+
control.providerSessionObserved = true;
|
|
708
|
+
if (control.providerTurnContentObserved) {
|
|
709
|
+
for (const requestId of control.durableRequestIds ?? []) {
|
|
710
|
+
markRuntimeUserTurnProviderSessionObserved(topicId, requestId, event.sessionId);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
675
713
|
// Persist visible topic turns only. Silent ask_session forks use
|
|
676
714
|
// temporary session ids and must not clobber the topic's durable
|
|
677
715
|
// session; visible tell/reply injects do belong to the main topic
|
|
@@ -788,7 +826,14 @@ export async function runTurnEventStream(
|
|
|
788
826
|
// Drain one deferred session-inject if the room is now idle. If a
|
|
789
827
|
// replacement turn already occupies the slot, leave it queued — that
|
|
790
828
|
// turn's finally will drain it when it completes.
|
|
791
|
-
|
|
829
|
+
const queuedUserTurn = roomId === topicId ? getRuntimeUserTurnRequest(topicId) : null;
|
|
830
|
+
const hasReplacementUserTurn = queuedUserTurn !== null && queuedUserTurn.requestId !== queryId;
|
|
831
|
+
if (
|
|
832
|
+
roomId === topicId &&
|
|
833
|
+
outcome.kind !== "session-expired" &&
|
|
834
|
+
!getRoomQuery(topicId) &&
|
|
835
|
+
!hasReplacementUserTurn
|
|
836
|
+
) {
|
|
792
837
|
const next = takeDeferredInject(topicId);
|
|
793
838
|
if (next) redispatchInject(next);
|
|
794
839
|
}
|
|
@@ -69,7 +69,8 @@ import {
|
|
|
69
69
|
import {
|
|
70
70
|
flattenUserTurnAttachments,
|
|
71
71
|
legacyUserTurnEnvelope,
|
|
72
|
-
|
|
72
|
+
renderUserTurnBatch,
|
|
73
|
+
renderUserTurnPrompt,
|
|
73
74
|
type UserTurnEnvelope,
|
|
74
75
|
} from "#runtime/user-turn-envelope";
|
|
75
76
|
import { getActiveVisualForPrompt } from "#runtime/visual-store";
|
|
@@ -119,7 +120,7 @@ export * from "#runtime/channel-context";
|
|
|
119
120
|
export * from "#runtime/errors";
|
|
120
121
|
export * from "#runtime/tasks";
|
|
121
122
|
export * from "#runtime/turn-session";
|
|
122
|
-
export { renderUserPromptBatch } from "#runtime/user-turn-envelope";
|
|
123
|
+
export { renderUserPromptBatch, renderUserTurnBatch } from "#runtime/user-turn-envelope";
|
|
123
124
|
export * from "#runtime/visuals";
|
|
124
125
|
|
|
125
126
|
// In-flight AI turns are tracked room-keyed (topicId → control) in
|
|
@@ -321,6 +322,7 @@ function redispatchInject(inject: DeferredInject): void {
|
|
|
321
322
|
aiMention: topic.aiMention,
|
|
322
323
|
},
|
|
323
324
|
userId: inject.userId,
|
|
325
|
+
vaultUserId: inject.vaultUserId,
|
|
324
326
|
prompt: inject.prompt,
|
|
325
327
|
allowAutoContinue: false,
|
|
326
328
|
origin: inject.origin,
|
|
@@ -501,7 +503,12 @@ async function deliverAskError(queryId: string, sourceLabel: string, error: stri
|
|
|
501
503
|
}
|
|
502
504
|
}
|
|
503
505
|
|
|
504
|
-
|
|
506
|
+
export type TopicRolloutReconstruction =
|
|
507
|
+
| { kind: "rebuilt"; sessionId: string }
|
|
508
|
+
| { kind: "no-history" }
|
|
509
|
+
| { kind: "failed"; error: string };
|
|
510
|
+
|
|
511
|
+
export function tryReconstructTopicRollout(opts: {
|
|
505
512
|
topicId: string;
|
|
506
513
|
topicTitle: string;
|
|
507
514
|
userId: string;
|
|
@@ -510,14 +517,12 @@ function tryReconstructTopicRollout(opts: {
|
|
|
510
517
|
cwd: string;
|
|
511
518
|
model: string;
|
|
512
519
|
effort?: EffortLevel;
|
|
513
|
-
|
|
514
|
-
}): string | null {
|
|
520
|
+
}): TopicRolloutReconstruction {
|
|
515
521
|
const { topicId, topicTitle, userId, agent, sessionId, cwd, model, effort } = opts;
|
|
516
|
-
if (!sessionId && !opts.allowFreshSession) return null;
|
|
517
522
|
|
|
518
523
|
try {
|
|
519
524
|
const entries = readConversation(userId, topicTitle);
|
|
520
|
-
if (entries.length === 0) return
|
|
525
|
+
if (entries.length === 0) return { kind: "no-history" };
|
|
521
526
|
const result = getRegistryOperations(agent).writeRollout({
|
|
522
527
|
cwd,
|
|
523
528
|
entries,
|
|
@@ -537,14 +542,18 @@ function tryReconstructTopicRollout(opts: {
|
|
|
537
542
|
? "ai: session expired — rollout reconstructed from unified log"
|
|
538
543
|
: "ai: provider session bridged from shared conversation log",
|
|
539
544
|
);
|
|
540
|
-
return result.sessionId;
|
|
545
|
+
return { kind: "rebuilt", sessionId: result.sessionId };
|
|
541
546
|
} catch (err) {
|
|
542
547
|
logger.warn({ err, topicId, agent, sessionId }, "ai: session reconstruct failed");
|
|
543
|
-
return
|
|
548
|
+
return { kind: "failed", error: stringifyError(err) };
|
|
544
549
|
}
|
|
545
550
|
}
|
|
546
551
|
|
|
547
|
-
|
|
552
|
+
export type SessionRetryResolution =
|
|
553
|
+
| { kind: "retry"; sessionId: string | null }
|
|
554
|
+
| { kind: "failed"; error: string };
|
|
555
|
+
|
|
556
|
+
export function resolveSessionRetry(opts: {
|
|
548
557
|
topicId: string;
|
|
549
558
|
topicTitle: string;
|
|
550
559
|
userId: string;
|
|
@@ -556,9 +565,12 @@ function resolveSessionRetryId(opts: {
|
|
|
556
565
|
effort?: EffortLevel;
|
|
557
566
|
externalSessionOwner?: boolean;
|
|
558
567
|
onSessionReset?: () => void;
|
|
559
|
-
}):
|
|
568
|
+
}): SessionRetryResolution {
|
|
560
569
|
const reconstructed = tryReconstructTopicRollout(opts);
|
|
561
|
-
if (reconstructed)
|
|
570
|
+
if (reconstructed.kind === "rebuilt") {
|
|
571
|
+
return { kind: "retry", sessionId: reconstructed.sessionId };
|
|
572
|
+
}
|
|
573
|
+
if (reconstructed.kind === "failed") return reconstructed;
|
|
562
574
|
if (opts.onSessionReset) opts.onSessionReset();
|
|
563
575
|
else if (!opts.silent && !opts.externalSessionOwner) {
|
|
564
576
|
clearTopicSessionId(opts.topicId, "session-expired");
|
|
@@ -567,7 +579,7 @@ function resolveSessionRetryId(opts: {
|
|
|
567
579
|
{ topicId: opts.topicId, agent: opts.agent, hadSessionId: Boolean(opts.sessionId) },
|
|
568
580
|
"ai: session expired — retrying with fresh session",
|
|
569
581
|
);
|
|
570
|
-
return null;
|
|
582
|
+
return { kind: "retry", sessionId: null };
|
|
571
583
|
}
|
|
572
584
|
|
|
573
585
|
export interface AiTurnSettlement {
|
|
@@ -630,6 +642,8 @@ export interface AiTurnExecutionOptions {
|
|
|
630
642
|
export interface StartAiTurnParams extends AiTurnExecutionOptions {
|
|
631
643
|
topic: AiTurnTopic;
|
|
632
644
|
userId: string;
|
|
645
|
+
/** Credential namespace. Defaults to userId for backwards compatibility. */
|
|
646
|
+
vaultUserId?: string;
|
|
633
647
|
prompt: string;
|
|
634
648
|
allowAutoContinue: boolean;
|
|
635
649
|
agentOverride?: AgentKind;
|
|
@@ -675,7 +689,7 @@ export function mergeSupersedingUserTurn(
|
|
|
675
689
|
...(incoming.userMessages ?? [legacyUserTurnEnvelope(incoming.prompt, incoming.attachments)]),
|
|
676
690
|
];
|
|
677
691
|
return {
|
|
678
|
-
prompt:
|
|
692
|
+
prompt: renderUserTurnBatch(userMessages),
|
|
679
693
|
userMessages,
|
|
680
694
|
attachments: flattenUserTurnAttachments(userMessages),
|
|
681
695
|
sessionId: running.sessionId,
|
|
@@ -710,13 +724,18 @@ function serializableUserTurnExecution(params: StartAiTurnParams): RuntimeUserTu
|
|
|
710
724
|
bridgeSessionFromHistory: params.bridgeSessionFromHistory,
|
|
711
725
|
peerBridge: params.peerBridge,
|
|
712
726
|
from: params.from,
|
|
727
|
+
vaultUserId: params.vaultUserId,
|
|
713
728
|
conversationPrompts: params._conversationPrompts ??
|
|
714
729
|
params._userMessages?.map((message) => message.prompt) ?? [params.prompt],
|
|
715
730
|
loggedUserMessageCount: params._loggedUserMessageCount,
|
|
716
731
|
};
|
|
717
732
|
}
|
|
718
733
|
|
|
719
|
-
function waitToStartRemoteUserTurn(
|
|
734
|
+
function waitToStartRemoteUserTurn(
|
|
735
|
+
params: StartAiTurnParams,
|
|
736
|
+
queryId: string,
|
|
737
|
+
options?: { omitRequestIds?: string[] },
|
|
738
|
+
): string {
|
|
720
739
|
const execution = serializableUserTurnExecution(params);
|
|
721
740
|
const incomingUserMessages = params._userMessages ?? [
|
|
722
741
|
legacyUserTurnEnvelope(params.prompt, params.attachments),
|
|
@@ -730,6 +749,7 @@ function waitToStartRemoteUserTurn(params: StartAiTurnParams, queryId: string):
|
|
|
730
749
|
execution,
|
|
731
750
|
topicEpoch: execution.runtimeEpoch ?? getRuntimeTopicEpoch(params.topic.id),
|
|
732
751
|
alreadyIncludedRequestIds: params._durableRequestIds,
|
|
752
|
+
omitRequestIds: options?.omitRequestIds,
|
|
733
753
|
});
|
|
734
754
|
for (const supersededRequestId of merged.supersededRequestIds) {
|
|
735
755
|
if (supersededRequestId !== merged.requestId) {
|
|
@@ -785,6 +805,7 @@ async function drainOneDurableUserTurn(): Promise<void> {
|
|
|
785
805
|
const queryId = startAiTurn({
|
|
786
806
|
topic,
|
|
787
807
|
userId: request.userId,
|
|
808
|
+
vaultUserId: execution?.vaultUserId,
|
|
788
809
|
prompt: request.prompt,
|
|
789
810
|
_userMessages: request.userMessages,
|
|
790
811
|
_conversationPrompts: execution?.conversationPrompts ?? [request.prompt],
|
|
@@ -873,6 +894,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
873
894
|
}
|
|
874
895
|
const topic: TopicDto = storedTopic;
|
|
875
896
|
const { userId, allowAutoContinue, onDispatched } = params;
|
|
897
|
+
const vaultUserId = params.vaultUserId ?? userId;
|
|
876
898
|
const origin = params.origin ?? "user";
|
|
877
899
|
const conversationPrompts = params._conversationPrompts ?? [params.prompt];
|
|
878
900
|
let loggedUserMessageCount = params._loggedUserMessageCount;
|
|
@@ -880,9 +902,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
880
902
|
let userMessages = isUserOrigin(origin)
|
|
881
903
|
? (params._userMessages ?? [legacyUserTurnEnvelope(params.prompt, params.attachments)])
|
|
882
904
|
: undefined;
|
|
883
|
-
let prompt = userMessages
|
|
884
|
-
? renderUserPromptBatch(userMessages.map((message) => message.prompt))
|
|
885
|
-
: params.prompt;
|
|
905
|
+
let prompt = userMessages ? renderUserTurnBatch(userMessages) : params.prompt;
|
|
886
906
|
let attachments = userMessages ? flattenUserTurnAttachments(userMessages) : params.attachments;
|
|
887
907
|
const execution = resolveTopicTurnExecution(topic, params);
|
|
888
908
|
const sessionResolution = resolveTopicTurnSession(topic, params.sessionId, {
|
|
@@ -953,6 +973,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
953
973
|
topicId,
|
|
954
974
|
runtimeEpoch,
|
|
955
975
|
userId,
|
|
976
|
+
vaultUserId,
|
|
956
977
|
prompt,
|
|
957
978
|
origin,
|
|
958
979
|
sourceNode,
|
|
@@ -1051,18 +1072,27 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1051
1072
|
}
|
|
1052
1073
|
if (decision.action === "abort-replace") {
|
|
1053
1074
|
const running = decision.running;
|
|
1075
|
+
let omitRequestIds: string[] | undefined;
|
|
1054
1076
|
if (isUserOrigin(running.origin)) {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1077
|
+
if (running.providerTurnContentObserved && running.sessionId) {
|
|
1078
|
+
// The native rollout already contains the interrupted user turn and
|
|
1079
|
+
// any partial assistant/tool history. Send only the new envelope after
|
|
1080
|
+
// the old provider process has exited; replaying the prior prompt makes
|
|
1081
|
+
// the model restart the work and can race two writers on one rollout.
|
|
1082
|
+
sessionId = running.sessionId;
|
|
1083
|
+
omitRequestIds = running.durableRequestIds;
|
|
1084
|
+
} else {
|
|
1085
|
+
const merged = mergeSupersedingUserTurn(running, { prompt, userMessages, attachments });
|
|
1086
|
+
userMessages = merged.userMessages;
|
|
1087
|
+
prompt = merged.prompt;
|
|
1088
|
+
attachments = merged.attachments;
|
|
1089
|
+
// No provider session was observed, so replay from the pre-turn base.
|
|
1090
|
+
sessionId = merged.sessionId;
|
|
1091
|
+
loggedUserMessageCount = running.userMessages?.length ?? 1;
|
|
1092
|
+
durableRequestIds = [
|
|
1093
|
+
...new Set([...(running.durableRequestIds ?? []), ...durableRequestIds]),
|
|
1094
|
+
];
|
|
1095
|
+
}
|
|
1066
1096
|
}
|
|
1067
1097
|
// The preempted turn was itself a session-inject → re-queue it so the
|
|
1068
1098
|
// inter-session work isn't lost; it resumes after the user's turn.
|
|
@@ -1095,6 +1125,28 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1095
1125
|
{ topicId, supersededQueryId: running.queryId, supersededOrigin: running.origin },
|
|
1096
1126
|
"ai: new user message superseded running turn",
|
|
1097
1127
|
);
|
|
1128
|
+
// Do not synchronously start another provider against the same native
|
|
1129
|
+
// session while the aborted process is still flushing its rollout. The
|
|
1130
|
+
// durable worker can claim this replacement only after the old room lease
|
|
1131
|
+
// is released in streamAgentEvents' finally block.
|
|
1132
|
+
const queuedQueryId = waitToStartRemoteUserTurn(
|
|
1133
|
+
{
|
|
1134
|
+
...params,
|
|
1135
|
+
topic,
|
|
1136
|
+
prompt,
|
|
1137
|
+
attachments,
|
|
1138
|
+
sessionId,
|
|
1139
|
+
_userMessages: userMessages,
|
|
1140
|
+
_conversationPrompts: conversationPrompts,
|
|
1141
|
+
_loggedUserMessageCount: loggedUserMessageCount,
|
|
1142
|
+
_durableRequestIds: durableRequestIds,
|
|
1143
|
+
_runtimeEpoch: runtimeEpoch,
|
|
1144
|
+
},
|
|
1145
|
+
queryId,
|
|
1146
|
+
{ omitRequestIds },
|
|
1147
|
+
);
|
|
1148
|
+
announceQueuedUserTurn({ ...params, topic }, queuedQueryId);
|
|
1149
|
+
return queuedQueryId;
|
|
1098
1150
|
}
|
|
1099
1151
|
|
|
1100
1152
|
const abortController = new AbortController();
|
|
@@ -1117,6 +1169,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1117
1169
|
topicId,
|
|
1118
1170
|
runtimeEpoch,
|
|
1119
1171
|
userId,
|
|
1172
|
+
vaultUserId,
|
|
1120
1173
|
prompt,
|
|
1121
1174
|
origin,
|
|
1122
1175
|
requestId,
|
|
@@ -1203,8 +1256,9 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1203
1256
|
|
|
1204
1257
|
const workspaceCwd = cwd ?? workspaceCwdFor(topicId);
|
|
1205
1258
|
mkdirSync(workspaceCwd, { recursive: true });
|
|
1259
|
+
let historyBridgeError: string | undefined;
|
|
1206
1260
|
if (bridgeSessionFromHistory && !sessionId) {
|
|
1207
|
-
const
|
|
1261
|
+
const bridge = tryReconstructTopicRollout({
|
|
1208
1262
|
topicId,
|
|
1209
1263
|
topicTitle: sessionName,
|
|
1210
1264
|
userId,
|
|
@@ -1213,20 +1267,22 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1213
1267
|
cwd: workspaceCwd,
|
|
1214
1268
|
model: resolvedModel,
|
|
1215
1269
|
...(resolvedEffort ? { effort: resolvedEffort } : {}),
|
|
1216
|
-
allowFreshSession: true,
|
|
1217
1270
|
});
|
|
1218
|
-
if (
|
|
1219
|
-
sessionId =
|
|
1220
|
-
control.sessionId =
|
|
1221
|
-
if (control.injectParams) control.injectParams.sessionId =
|
|
1271
|
+
if (bridge.kind === "rebuilt") {
|
|
1272
|
+
sessionId = bridge.sessionId;
|
|
1273
|
+
control.sessionId = bridge.sessionId;
|
|
1274
|
+
if (control.injectParams) control.injectParams.sessionId = bridge.sessionId;
|
|
1222
1275
|
try {
|
|
1223
|
-
onSessionId?.(
|
|
1276
|
+
onSessionId?.(bridge.sessionId);
|
|
1224
1277
|
} catch (err) {
|
|
1225
1278
|
logger.warn(
|
|
1226
1279
|
{ err, topicId, queryId, agent: agentKind },
|
|
1227
1280
|
"ai: isolated session owner rejected bridged provider session id",
|
|
1228
1281
|
);
|
|
1229
1282
|
}
|
|
1283
|
+
} else if (bridge.kind === "failed") {
|
|
1284
|
+
historyBridgeError =
|
|
1285
|
+
"Conversation history could not be encoded into a provider session; no fresh session was started.";
|
|
1230
1286
|
}
|
|
1231
1287
|
}
|
|
1232
1288
|
const messageAttachmentGroups = userMessages?.map((message, index) =>
|
|
@@ -1235,15 +1291,19 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1235
1291
|
const promptAttachments =
|
|
1236
1292
|
messageAttachmentGroups?.flat() ?? materializePromptAttachments(topicId, queryId, attachments);
|
|
1237
1293
|
const promptWithFiles = userMessages
|
|
1238
|
-
?
|
|
1239
|
-
userMessages.map((message, index) =>
|
|
1240
|
-
|
|
1241
|
-
|
|
1294
|
+
? renderUserTurnBatch(
|
|
1295
|
+
userMessages.map((message, index) => ({
|
|
1296
|
+
...message,
|
|
1297
|
+
prompt: promptWithAttachments(message.prompt, messageAttachmentGroups?.[index] ?? []),
|
|
1298
|
+
})),
|
|
1242
1299
|
)
|
|
1243
1300
|
: promptWithAttachments(prompt, promptAttachments);
|
|
1244
1301
|
const renderedUserPrompts = userMessages
|
|
1245
1302
|
? userMessages.map((message, index) =>
|
|
1246
|
-
|
|
1303
|
+
renderUserTurnPrompt({
|
|
1304
|
+
...message,
|
|
1305
|
+
prompt: promptWithAttachments(message.prompt, messageAttachmentGroups?.[index] ?? []),
|
|
1306
|
+
}),
|
|
1247
1307
|
)
|
|
1248
1308
|
: undefined;
|
|
1249
1309
|
const agentPrompt =
|
|
@@ -1419,6 +1479,10 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1419
1479
|
: enabledMcp.filter((name) => name !== "playwright");
|
|
1420
1480
|
const wantsBgBash = !isManager && enabledMcp.includes("background-bash");
|
|
1421
1481
|
async function* runWithPlaywright(): AsyncGenerator<UnifiedEvent> {
|
|
1482
|
+
if (historyBridgeError) {
|
|
1483
|
+
yield { type: "error", content: historyBridgeError };
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1422
1486
|
if (prepareSession && !sessionId) {
|
|
1423
1487
|
if (abortController.signal.aborted) return;
|
|
1424
1488
|
try {
|
|
@@ -1495,6 +1559,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1495
1559
|
systemPrompt: effectiveSystemPrompt,
|
|
1496
1560
|
sessionId,
|
|
1497
1561
|
userId,
|
|
1562
|
+
vaultUserId,
|
|
1498
1563
|
session: sessionName,
|
|
1499
1564
|
sessionType: sessionType ?? (isManager ? "manager" : "forum"),
|
|
1500
1565
|
abortController,
|
|
@@ -1557,10 +1622,10 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1557
1622
|
onSessionId,
|
|
1558
1623
|
{ silent, peerBridge, sourceNode },
|
|
1559
1624
|
)
|
|
1560
|
-
.then(async (
|
|
1625
|
+
.then(async (streamOutcome) => {
|
|
1626
|
+
let outcome = streamOutcome;
|
|
1561
1627
|
if (outcome.kind === "session-expired") {
|
|
1562
|
-
|
|
1563
|
-
const retrySessionId = resolveSessionRetryId({
|
|
1628
|
+
const retry = resolveSessionRetry({
|
|
1564
1629
|
topicId,
|
|
1565
1630
|
topicTitle: sessionName,
|
|
1566
1631
|
userId,
|
|
@@ -1573,63 +1638,74 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1573
1638
|
externalSessionOwner: Boolean(onSessionId),
|
|
1574
1639
|
onSessionReset,
|
|
1575
1640
|
});
|
|
1576
|
-
|
|
1577
|
-
{
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1641
|
+
if (retry.kind === "failed") {
|
|
1642
|
+
outcome = {
|
|
1643
|
+
kind: "provider-error",
|
|
1644
|
+
error:
|
|
1645
|
+
"Provider session expired and conversation history could not be rebuilt; the existing session was preserved for retry.",
|
|
1646
|
+
};
|
|
1647
|
+
} else {
|
|
1648
|
+
if (!silent) WsHub.get().broadcastAborted(topicId, queryId, "stopped");
|
|
1649
|
+
const retrySessionId = retry.sessionId;
|
|
1650
|
+
logger.info(
|
|
1651
|
+
{
|
|
1652
|
+
topicId,
|
|
1653
|
+
prevQueryId: queryId,
|
|
1654
|
+
agent: agentKind,
|
|
1655
|
+
retrySessionId: retrySessionId ? retrySessionId.slice(0, 8) : null,
|
|
1656
|
+
},
|
|
1657
|
+
"ai: retrying query after session expiry",
|
|
1658
|
+
);
|
|
1659
|
+
if (!retrySessionId && prepareSession && forkHandle) {
|
|
1660
|
+
// The retry will invoke the recipe and mint a different rollout.
|
|
1661
|
+
// The expired fork is no longer in use after stream cleanup.
|
|
1662
|
+
cleanupAgentFork(forkHandle);
|
|
1663
|
+
forkHandle = undefined;
|
|
1664
|
+
}
|
|
1665
|
+
startAiTurn({
|
|
1666
|
+
topic,
|
|
1667
|
+
userId,
|
|
1668
|
+
vaultUserId,
|
|
1669
|
+
prompt,
|
|
1670
|
+
_userMessages: userMessages,
|
|
1671
|
+
_conversationPrompts: conversationPrompts,
|
|
1672
|
+
_loggedUserMessageCount: loggedUserMessageCount,
|
|
1673
|
+
_durableRequestIds: durableRequestIds,
|
|
1674
|
+
attachments,
|
|
1675
|
+
allowAutoContinue,
|
|
1676
|
+
origin,
|
|
1677
|
+
onDispatched,
|
|
1678
|
+
requestId,
|
|
1679
|
+
depth,
|
|
1680
|
+
silent,
|
|
1681
|
+
contextId,
|
|
1682
|
+
agentOverride,
|
|
1683
|
+
modelOverride,
|
|
1684
|
+
effortOverride,
|
|
1685
|
+
sessionId: retrySessionId,
|
|
1686
|
+
sessionScope,
|
|
1687
|
+
turnConcurrency,
|
|
1688
|
+
forkHandle,
|
|
1689
|
+
// Keep the recipe so a later user preemption can replay from a
|
|
1690
|
+
// clean snapshot. A non-empty retrySessionId suppresses immediate
|
|
1691
|
+
// preparation in runWithPlaywright.
|
|
1692
|
+
prepareSession,
|
|
1693
|
+
cwd,
|
|
1694
|
+
sessionName,
|
|
1695
|
+
sessionType,
|
|
1696
|
+
visualTools,
|
|
1697
|
+
fileDeliveryTools,
|
|
1698
|
+
onSessionId,
|
|
1699
|
+
onSessionReset,
|
|
1700
|
+
bridgeSessionFromHistory,
|
|
1701
|
+
onSettled,
|
|
1702
|
+
peerBridge,
|
|
1703
|
+
askReplySources,
|
|
1704
|
+
_runtimeEpoch: runtimeEpoch,
|
|
1705
|
+
_sessionRetried: true,
|
|
1706
|
+
});
|
|
1707
|
+
return;
|
|
1590
1708
|
}
|
|
1591
|
-
startAiTurn({
|
|
1592
|
-
topic,
|
|
1593
|
-
userId,
|
|
1594
|
-
prompt,
|
|
1595
|
-
_userMessages: userMessages,
|
|
1596
|
-
_conversationPrompts: conversationPrompts,
|
|
1597
|
-
_loggedUserMessageCount: loggedUserMessageCount,
|
|
1598
|
-
_durableRequestIds: durableRequestIds,
|
|
1599
|
-
attachments,
|
|
1600
|
-
allowAutoContinue,
|
|
1601
|
-
origin,
|
|
1602
|
-
onDispatched,
|
|
1603
|
-
requestId,
|
|
1604
|
-
depth,
|
|
1605
|
-
silent,
|
|
1606
|
-
contextId,
|
|
1607
|
-
agentOverride,
|
|
1608
|
-
modelOverride,
|
|
1609
|
-
effortOverride,
|
|
1610
|
-
sessionId: retrySessionId,
|
|
1611
|
-
sessionScope,
|
|
1612
|
-
turnConcurrency,
|
|
1613
|
-
forkHandle,
|
|
1614
|
-
// Keep the recipe so a later user preemption can replay from a
|
|
1615
|
-
// clean snapshot. A non-empty retrySessionId suppresses immediate
|
|
1616
|
-
// preparation in runWithPlaywright.
|
|
1617
|
-
prepareSession,
|
|
1618
|
-
cwd,
|
|
1619
|
-
sessionName,
|
|
1620
|
-
sessionType,
|
|
1621
|
-
visualTools,
|
|
1622
|
-
fileDeliveryTools,
|
|
1623
|
-
onSessionId,
|
|
1624
|
-
onSessionReset,
|
|
1625
|
-
bridgeSessionFromHistory,
|
|
1626
|
-
onSettled,
|
|
1627
|
-
peerBridge,
|
|
1628
|
-
askReplySources,
|
|
1629
|
-
_runtimeEpoch: runtimeEpoch,
|
|
1630
|
-
_sessionRetried: true,
|
|
1631
|
-
});
|
|
1632
|
-
return;
|
|
1633
1709
|
}
|
|
1634
1710
|
|
|
1635
1711
|
if (outcome.kind === "provider-error") {
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
export interface UserTurnEnvelope {
|
|
3
3
|
prompt: string;
|
|
4
4
|
attachments?: string[];
|
|
5
|
+
/** Authenticated human author. Execution may still run as the local principal. */
|
|
6
|
+
actorUserId?: string;
|
|
7
|
+
/** Display-only author label captured by the trusted ingress. */
|
|
8
|
+
actorLabel?: string;
|
|
5
9
|
}
|
|
6
10
|
|
|
7
11
|
export function legacyUserTurnEnvelope(prompt: string, attachments?: string[]): UserTurnEnvelope {
|
|
@@ -23,3 +27,18 @@ export function renderUserPromptBatch(prompts: readonly string[]): string {
|
|
|
23
27
|
...prompts.map((prompt, index) => `${index + 1}. ${prompt}`),
|
|
24
28
|
].join("\n");
|
|
25
29
|
}
|
|
30
|
+
|
|
31
|
+
export function renderUserTurnPrompt(message: UserTurnEnvelope): string {
|
|
32
|
+
const rawLabel = message.actorLabel?.trim() || message.actorUserId?.trim();
|
|
33
|
+
const label = rawLabel
|
|
34
|
+
?.replace(/[\r\n\t]+/g, " ")
|
|
35
|
+
.replace(/\[/g, "(")
|
|
36
|
+
.replace(/\]/g, ")")
|
|
37
|
+
.replace(/\s{2,}/g, " ");
|
|
38
|
+
return label ? `[@${label}]: ${message.prompt}` : message.prompt;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Render ordered user submissions while retaining their structured authorship. */
|
|
42
|
+
export function renderUserTurnBatch(messages: readonly UserTurnEnvelope[]): string {
|
|
43
|
+
return renderUserPromptBatch(messages.map(renderUserTurnPrompt));
|
|
44
|
+
}
|