negotium 0.19.2 → 0.20.0
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 +1146 -212
- package/dist/agent-helpers.js.map +27 -19
- package/dist/background-bash.js.map +1 -1
- package/dist/browser-runtime.js.map +1 -1
- package/dist/{chunk-kkftbxh2.js → chunk-7he9nzfa.js} +1 -1
- package/dist/{chunk-55dd0xw7.js → chunk-ad6xqvqy.js} +2 -2
- package/dist/{chunk-55dd0xw7.js.map → chunk-ad6xqvqy.js.map} +2 -2
- package/dist/{chunk-exqz8bez.js → chunk-hh3f0yr7.js} +3 -3
- package/dist/{chunk-exqz8bez.js.map → chunk-hh3f0yr7.js.map} +2 -2
- package/dist/{chunk-aqqmzmft.js → chunk-nm59q3em.js} +1 -1
- package/dist/{chunk-cv7vdsrn.js → chunk-w3c6fc0p.js} +286 -9
- package/dist/{chunk-cv7vdsrn.js.map → chunk-w3c6fc0p.js.map} +10 -7
- package/dist/hosted-agent.js +290 -12
- package/dist/hosted-agent.js.map +10 -7
- package/dist/main.js +4599 -2724
- package/dist/main.js.map +49 -38
- package/dist/mcp-factories.js +1166 -162
- package/dist/mcp-factories.js.map +27 -19
- package/dist/media.js.map +1 -1
- package/dist/prompts.js.map +1 -1
- package/dist/query-runtime.js.map +3 -3
- package/dist/registry.js +3 -3
- package/dist/rollout.js +2 -2
- package/dist/runtime/src/agents/explicit-agent-switch.ts +101 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +126 -43
- package/dist/runtime/src/agents/public-helpers.ts +5 -0
- package/dist/runtime/src/agents/self-config-core.ts +28 -58
- package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +34 -8
- package/dist/runtime/src/index.ts +75 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +6 -1
- package/dist/runtime/src/mcp/runtime-spec.ts +145 -10
- package/dist/runtime/src/mcp/session-comm/actor-policy.ts +87 -0
- package/dist/runtime/src/mcp/session-comm/context.ts +42 -1
- package/dist/runtime/src/mcp/session-comm/default-host.ts +178 -2
- package/dist/runtime/src/mcp/session-comm/hub-remote-session.ts +553 -0
- package/dist/runtime/src/mcp/session-comm/limits.ts +8 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +78 -3
- package/dist/runtime/src/mcp/session-comm/server.ts +167 -4
- package/dist/runtime/src/mcp/session-comm/topic-catalog.ts +7 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +32 -10
- package/dist/runtime/src/mcp-runtime-host.ts +2 -1
- package/dist/runtime/src/node-host.ts +26 -0
- package/dist/runtime/src/platform/mcp-config.ts +56 -2
- package/dist/runtime/src/query/active-rooms.ts +6 -0
- package/dist/runtime/src/runtime/actor-topic-reach.ts +85 -0
- package/dist/runtime/src/runtime/actor-topic-scope.ts +175 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +73 -20
- package/dist/runtime/src/runtime/gateway-capabilities.ts +22 -0
- package/dist/runtime/src/runtime/remote-session-grant.ts +221 -0
- package/dist/runtime/src/runtime/remote-session-inbox.ts +476 -0
- package/dist/runtime/src/runtime/remote-session-reply-outbox.ts +156 -0
- package/dist/runtime/src/runtime/turn-runner.ts +157 -33
- package/dist/runtime/src/runtime-gateway.ts +20 -0
- package/dist/runtime/src/storage/api-topics.ts +22 -3
- package/dist/runtime/src/storage/remote-session.ts +526 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +75 -2
- package/dist/runtime/src/topics/derive.ts +3 -0
- package/dist/runtime/src/types/api.ts +6 -0
- package/dist/runtime/src/types.ts +42 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-gateway.js.map +2 -2
- package/dist/runtime-helpers.js.map +1 -1
- package/dist/storage.js +16 -4
- package/dist/storage.js.map +4 -4
- package/dist/types/packages/core/src/agents/explicit-agent-switch.d.ts +26 -0
- package/dist/types/packages/core/src/agents/mcp-tools/spawn-subagent.d.ts +12 -2
- package/dist/types/packages/core/src/agents/public-helpers.d.ts +1 -0
- package/dist/types/packages/core/src/agents/self-config-core.d.ts +15 -1
- package/dist/types/packages/core/src/mcp/peer-bridge.d.ts +6 -1
- package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +69 -2
- package/dist/types/packages/core/src/mcp/session-comm/context.d.ts +17 -1
- package/dist/types/packages/core/src/mcp/session-comm/hub-remote-session.d.ts +149 -0
- package/dist/types/packages/core/src/mcp/session-comm/limits.d.ts +8 -0
- package/dist/types/packages/core/src/mcp/session-comm/peer-forward.d.ts +23 -2
- package/dist/types/packages/core/src/mcp/session-comm/topic-catalog.d.ts +6 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +10 -1
- package/dist/types/packages/core/src/query/active-rooms.d.ts +6 -0
- package/dist/types/packages/core/src/runtime/actor-topic-reach.d.ts +49 -0
- package/dist/types/packages/core/src/runtime/actor-topic-scope.d.ts +91 -0
- package/dist/types/packages/core/src/runtime/ask-callbacks.d.ts +8 -2
- package/dist/types/packages/core/src/runtime/remote-session-grant.d.ts +91 -0
- package/dist/types/packages/core/src/runtime/remote-session-inbox.d.ts +83 -0
- package/dist/types/packages/core/src/runtime/remote-session-reply-outbox.d.ts +37 -0
- package/dist/types/packages/core/src/runtime/turn-runner.d.ts +22 -3
- package/dist/types/packages/core/src/runtime-gateway.d.ts +20 -0
- package/dist/types/packages/core/src/storage/api-topics.d.ts +3 -0
- package/dist/types/packages/core/src/storage/remote-session.d.ts +135 -0
- package/dist/types/packages/core/src/storage/runtime-turn-requests.d.ts +12 -1
- package/dist/types/packages/core/src/topics/derive.d.ts +2 -0
- package/dist/types/packages/core/src/types/api.d.ts +6 -0
- package/dist/types/packages/core/src/types.d.ts +40 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js.map +1 -1
- package/package.json +1 -1
- /package/dist/{chunk-kkftbxh2.js.map → chunk-7he9nzfa.js.map} +0 -0
- /package/dist/{chunk-aqqmzmft.js.map → chunk-nm59q3em.js.map} +0 -0
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
} from "#query/active-rooms";
|
|
52
52
|
import { writeQueryState } from "#query/state";
|
|
53
53
|
import { AbortReason } from "#query/types";
|
|
54
|
+
import { actorTopicScopeFrom } from "#runtime/actor-topic-scope";
|
|
54
55
|
import {
|
|
55
56
|
materializePromptAttachments,
|
|
56
57
|
promptWithAttachments,
|
|
@@ -60,6 +61,7 @@ import { buildMentionOnlyChannelPrompt } from "#runtime/channel-context";
|
|
|
60
61
|
import { classifyAgentError, stringifyError } from "#runtime/errors";
|
|
61
62
|
import { withTurnSilenceHeartbeat } from "#runtime/event-heartbeat";
|
|
62
63
|
import { trackPlaywrightTurn, untrackPlaywrightTurn } from "#runtime/playwright-turn-abort";
|
|
64
|
+
import { remoteSessionGrantFrom } from "#runtime/remote-session-grant";
|
|
63
65
|
import { getTopicConfig } from "#runtime/topic-config";
|
|
64
66
|
import { runTurnEventStream, type StreamAgentOutcome } from "#runtime/turn-event-stream";
|
|
65
67
|
import {
|
|
@@ -87,6 +89,7 @@ import {
|
|
|
87
89
|
import { getGlobalAiName } from "#storage/app-settings";
|
|
88
90
|
import { isTopicBrowserProfileOwner } from "#storage/browser-profiles";
|
|
89
91
|
import { appendConversationEvent, readConversation } from "#storage/conversations";
|
|
92
|
+
import { db } from "#storage/forum-db";
|
|
90
93
|
import {
|
|
91
94
|
getRuntimeTurnLease,
|
|
92
95
|
RUNTIME_INSTANCE_ID,
|
|
@@ -96,6 +99,7 @@ import { getRuntimeTopicEpoch, isRuntimeTopicMaintenance } from "#storage/runtim
|
|
|
96
99
|
import {
|
|
97
100
|
claimNextRuntimeUserTurnRequest,
|
|
98
101
|
completeRuntimeUserTurnRequest,
|
|
102
|
+
getActiveRuntimeUserTurnRequest,
|
|
99
103
|
markRuntimeUserTurnMessagesLogged,
|
|
100
104
|
markRuntimeUserTurnRunning,
|
|
101
105
|
mergeRuntimeUserTurnRequest,
|
|
@@ -113,7 +117,14 @@ import {
|
|
|
113
117
|
wikiSummaryFilename,
|
|
114
118
|
} from "#storage/wiki-summary-names";
|
|
115
119
|
import { getTopics } from "#topics/derive";
|
|
116
|
-
import type {
|
|
120
|
+
import type {
|
|
121
|
+
ActorTopicScope,
|
|
122
|
+
AgentKind,
|
|
123
|
+
EffortLevel,
|
|
124
|
+
PeerRuntimeBridgeContext,
|
|
125
|
+
RemoteSessionGrant,
|
|
126
|
+
UnifiedEvent,
|
|
127
|
+
} from "#types";
|
|
117
128
|
import type { MessageDto, TopicDto } from "#types/api";
|
|
118
129
|
|
|
119
130
|
// 분해된 헬퍼 모듈 재노출 — 기존 @/api/routes/ai 소비자(테스트 포함) 경로 유지
|
|
@@ -439,7 +450,10 @@ async function markPendingAskFile(
|
|
|
439
450
|
}
|
|
440
451
|
|
|
441
452
|
/** Caller-side injection of an ask reply — delivers the target AI's answer (or
|
|
442
|
-
* error) back into the caller topic, triggering its AI when one is invited.
|
|
453
|
+
* error) back into the caller topic, triggering its AI when one is invited.
|
|
454
|
+
* `options.onRecorded` runs inside the same storage transaction as the durable
|
|
455
|
+
* room record (the appended reply message), so a caller that must consume its
|
|
456
|
+
* own bookkeeping exactly when the answer is durable can do so atomically. */
|
|
443
457
|
export async function deliverAskCallbackToCaller(
|
|
444
458
|
pending: AskPendingFileRef & {
|
|
445
459
|
requestId: string;
|
|
@@ -451,28 +465,36 @@ export async function deliverAskCallbackToCaller(
|
|
|
451
465
|
sourceLabel: string,
|
|
452
466
|
body: string,
|
|
453
467
|
kind: "reply" | "error",
|
|
468
|
+
options: { onRecorded?: () => void } = {},
|
|
454
469
|
): Promise<boolean> {
|
|
455
470
|
if (pending.remoteReply) {
|
|
456
471
|
const { deliverPeerReply } = await import("#mcp/session-comm/peer-forward");
|
|
457
|
-
|
|
472
|
+
const delivered = await deliverPeerReply(pending.remoteReply, sourceLabel, body, kind);
|
|
473
|
+
if (delivered) options.onRecorded?.();
|
|
474
|
+
return delivered;
|
|
458
475
|
}
|
|
459
476
|
const callerTopic = getTopic(pending.callerTopicId);
|
|
460
477
|
const heading = kind === "error" ? `Error from ${sourceLabel}` : `Reply from ${sourceLabel}`;
|
|
461
478
|
const prompt = `[${heading}]\n\n${body}`;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (!callerTopic?.agent) {
|
|
466
|
-
try {
|
|
479
|
+
const record = (agentType?: AgentKind | null) =>
|
|
480
|
+
db.transaction(() => {
|
|
467
481
|
appendAskReplyMessage(
|
|
468
482
|
pending.callerTopicId,
|
|
469
483
|
prompt,
|
|
470
484
|
sourceLabel,
|
|
471
485
|
body,
|
|
472
486
|
kind,
|
|
473
|
-
undefined,
|
|
487
|
+
agentType ?? undefined,
|
|
474
488
|
pending.callerThreadRootId,
|
|
475
489
|
);
|
|
490
|
+
options.onRecorded?.();
|
|
491
|
+
})();
|
|
492
|
+
|
|
493
|
+
await markPendingAskFile(pending, "reply_ready");
|
|
494
|
+
|
|
495
|
+
if (!callerTopic?.agent) {
|
|
496
|
+
try {
|
|
497
|
+
record(undefined);
|
|
476
498
|
await clearPendingAskFile(pending);
|
|
477
499
|
return true;
|
|
478
500
|
} catch (err) {
|
|
@@ -513,22 +535,36 @@ export async function deliverAskCallbackToCaller(
|
|
|
513
535
|
if (queued) {
|
|
514
536
|
// Keep individual replies visible in topic history; only the model-facing
|
|
515
537
|
// caller turn is coalesced into one prompt.
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
538
|
+
try {
|
|
539
|
+
record(callerTopic.agent);
|
|
540
|
+
} catch (err) {
|
|
541
|
+
// The durable record is what makes this answer real — it is also the
|
|
542
|
+
// transaction that consumes the caller's bookkeeping (`onRecorded`).
|
|
543
|
+
// Without it the queued entry is the answer's ONLY trace, in memory, in
|
|
544
|
+
// a process that just failed a write: leaving it there makes the sender's
|
|
545
|
+
// retry hit the dedupe branch below and settle as if the answer had
|
|
546
|
+
// landed, so a crash loses it for good. Drop the entry instead and fail,
|
|
547
|
+
// so the retry runs the whole delivery again from a clean slate.
|
|
548
|
+
interSessionQueue.remove(pending.callerTopicId, pending.requestId);
|
|
549
|
+
logger.warn(
|
|
550
|
+
{ err, requestId: pending.requestId, callerTopicId: pending.callerTopicId },
|
|
551
|
+
"sessions: ask callback record failed after enqueue; queue entry withdrawn",
|
|
552
|
+
);
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
525
555
|
await markPendingAskFile(pending, "queued_for_caller");
|
|
526
556
|
return true;
|
|
527
557
|
}
|
|
528
558
|
|
|
529
559
|
// A replay can encounter the same request while its original callback is
|
|
530
560
|
// still queued. That request is already owned; do not append it twice.
|
|
561
|
+
// `onRecorded` still has to run atomically: it is an ask row deletion plus a
|
|
562
|
+
// claim completion, and a crash between the two would leave a completed
|
|
563
|
+
// claim with the ask still pending (or the reverse).
|
|
531
564
|
if (interSessionQueue.hasRequest(pending.callerTopicId, pending.requestId)) {
|
|
565
|
+
db.transaction(() => {
|
|
566
|
+
options.onRecorded?.();
|
|
567
|
+
})();
|
|
532
568
|
await markPendingAskFile(pending, "queued_for_caller");
|
|
533
569
|
return true;
|
|
534
570
|
}
|
|
@@ -537,15 +573,7 @@ export async function deliverAskCallbackToCaller(
|
|
|
537
573
|
{ requestId: pending.requestId, callerTopicId: pending.callerTopicId, source: sourceLabel },
|
|
538
574
|
"sessions: ask callback could not enter caller batch; appending direct fallback",
|
|
539
575
|
);
|
|
540
|
-
|
|
541
|
-
pending.callerTopicId,
|
|
542
|
-
prompt,
|
|
543
|
-
sourceLabel,
|
|
544
|
-
body,
|
|
545
|
-
kind,
|
|
546
|
-
callerTopic.agent,
|
|
547
|
-
pending.callerThreadRootId,
|
|
548
|
-
);
|
|
576
|
+
record(callerTopic.agent);
|
|
549
577
|
await clearPendingAskFile(pending);
|
|
550
578
|
return true;
|
|
551
579
|
}
|
|
@@ -663,6 +691,10 @@ export interface AiTurnExecutionOptions {
|
|
|
663
691
|
origin?: string;
|
|
664
692
|
/** Product actor identity when the node executes as a shared principal. */
|
|
665
693
|
actorUserId?: string;
|
|
694
|
+
/** Host-asserted rooms the actor may reach; absent confines cross-room tools. */
|
|
695
|
+
actorTopicScope?: ActorTopicScope;
|
|
696
|
+
/** Hub-issued per-turn remote session-comm grant; absent keeps it fail-closed. */
|
|
697
|
+
remoteSession?: RemoteSessionGrant;
|
|
666
698
|
/** Origin node for transcript echo suppression. */
|
|
667
699
|
sourceNode?: string;
|
|
668
700
|
/** Answer inside this thread instead of the room's main flow (S-13). */
|
|
@@ -789,6 +821,39 @@ export interface TriggerTopicAiTurnOptions extends AiTurnExecutionOptions {
|
|
|
789
821
|
|
|
790
822
|
const remoteInjectWaiters = new Map<string, ReturnType<typeof setInterval>>();
|
|
791
823
|
|
|
824
|
+
/**
|
|
825
|
+
* Whether a new human message comes from the person a running user turn is
|
|
826
|
+
* answering. Only that person may steer the turn (abort it and fold the new
|
|
827
|
+
* message into a resumed batch); anyone else's message queues behind it. The
|
|
828
|
+
* comparison mirrors the durable merge rule: two turns without a recorded
|
|
829
|
+
* actor are one conversation, but an unrecorded actor never matches a named
|
|
830
|
+
* one — the conservative reading, since the alternative is folding a
|
|
831
|
+
* stranger's message into a turn whose author is unknown.
|
|
832
|
+
*/
|
|
833
|
+
export function sameTurnActor(
|
|
834
|
+
runningActorUserId: string | undefined,
|
|
835
|
+
incomingActorUserId: string | undefined,
|
|
836
|
+
): boolean {
|
|
837
|
+
return (runningActorUserId ?? null) === (incomingActorUserId ?? null);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Whether the durable request another worker holds for `topicId` belongs to
|
|
842
|
+
* the same actor as an incoming user turn. `ownRequestIds` are the incoming
|
|
843
|
+
* turn's own durable rows, which the lookup must not mistake for the remote
|
|
844
|
+
* turn. No visible active request means the remote turn's actor is unknown,
|
|
845
|
+
* which is never "the same person".
|
|
846
|
+
*/
|
|
847
|
+
function remoteTurnHasSameActor(
|
|
848
|
+
topicId: string,
|
|
849
|
+
incomingActorUserId: string | undefined,
|
|
850
|
+
ownRequestIds: readonly string[],
|
|
851
|
+
): boolean {
|
|
852
|
+
const active = getActiveRuntimeUserTurnRequest(topicId);
|
|
853
|
+
if (!active || ownRequestIds.includes(active.requestId)) return false;
|
|
854
|
+
return sameTurnActor(active.execution?.actorUserId, incomingActorUserId);
|
|
855
|
+
}
|
|
856
|
+
|
|
792
857
|
export function mergeSupersedingUserTurn(
|
|
793
858
|
running: Pick<RoomQueryControl, "prompt" | "userMessages" | "attachments" | "sessionId">,
|
|
794
859
|
incoming: { prompt: string; userMessages?: UserTurnEnvelope[]; attachments?: string[] },
|
|
@@ -825,6 +890,8 @@ function serializableUserTurnExecution(params: StartAiTurnParams): RuntimeUserTu
|
|
|
825
890
|
runtimeEpoch: params._runtimeEpoch ?? getRuntimeTopicEpoch(params.topic.id),
|
|
826
891
|
sourceRequestId: params.requestId,
|
|
827
892
|
actorUserId: params.actorUserId,
|
|
893
|
+
actorTopicScope: params.actorTopicScope,
|
|
894
|
+
remoteSession: params.remoteSession,
|
|
828
895
|
agentOverride: params.agentOverride,
|
|
829
896
|
modelOverride: params.modelOverride,
|
|
830
897
|
effortOverride: params.effortOverride,
|
|
@@ -921,6 +988,8 @@ async function drainOneDurableUserTurn(): Promise<void> {
|
|
|
921
988
|
topic,
|
|
922
989
|
userId: request.userId,
|
|
923
990
|
actorUserId: execution?.actorUserId,
|
|
991
|
+
actorTopicScope: actorTopicScopeFrom(execution?.actorTopicScope),
|
|
992
|
+
remoteSession: remoteSessionGrantFrom(execution?.remoteSession),
|
|
924
993
|
vaultUserId: execution?.vaultUserId,
|
|
925
994
|
prompt: request.prompt,
|
|
926
995
|
_userMessages: request.userMessages,
|
|
@@ -1169,7 +1238,11 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1169
1238
|
return null;
|
|
1170
1239
|
}
|
|
1171
1240
|
if (decision.action === "remote-abort-wait") {
|
|
1172
|
-
|
|
1241
|
+
// Only the same person's follow-up steers the turn the other process is
|
|
1242
|
+
// running; anyone else's message just waits for the lease to be released.
|
|
1243
|
+
if (remoteTurnHasSameActor(topicId, params.actorUserId, durableRequestIds)) {
|
|
1244
|
+
requestRuntimeTurnAbort(topicId, "internal");
|
|
1245
|
+
}
|
|
1173
1246
|
const queuedQueryId = waitToStartRemoteUserTurn(
|
|
1174
1247
|
{
|
|
1175
1248
|
...params,
|
|
@@ -1193,6 +1266,33 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1193
1266
|
}
|
|
1194
1267
|
if (decision.action === "abort-replace") {
|
|
1195
1268
|
const running = decision.running;
|
|
1269
|
+
// Another person's running answer is not this message's to interrupt. The
|
|
1270
|
+
// message keeps its own durable row (the merge never folds across actors)
|
|
1271
|
+
// and the worker claims it once the running turn releases the room. A
|
|
1272
|
+
// running session-inject is still preempted by any human message.
|
|
1273
|
+
if (isUserOrigin(running.origin) && !sameTurnActor(running.actorUserId, params.actorUserId)) {
|
|
1274
|
+
const queuedQueryId = waitToStartRemoteUserTurn(
|
|
1275
|
+
{
|
|
1276
|
+
...params,
|
|
1277
|
+
topic,
|
|
1278
|
+
prompt,
|
|
1279
|
+
attachments,
|
|
1280
|
+
sessionId,
|
|
1281
|
+
_userMessages: userMessages,
|
|
1282
|
+
_conversationPrompts: conversationPrompts,
|
|
1283
|
+
_loggedUserMessageCount: loggedUserMessageCount,
|
|
1284
|
+
_durableRequestIds: durableRequestIds,
|
|
1285
|
+
_runtimeEpoch: runtimeEpoch,
|
|
1286
|
+
},
|
|
1287
|
+
queryId,
|
|
1288
|
+
);
|
|
1289
|
+
announceQueuedUserTurn({ ...params, topic }, queuedQueryId);
|
|
1290
|
+
logger.info(
|
|
1291
|
+
{ topicId, queryId: queuedQueryId, runningQueryId: running.queryId },
|
|
1292
|
+
"ai: user turn from another actor queued behind the running turn",
|
|
1293
|
+
);
|
|
1294
|
+
return queuedQueryId;
|
|
1295
|
+
}
|
|
1196
1296
|
let omitRequestIds: string[] | undefined;
|
|
1197
1297
|
if (isUserOrigin(running.origin)) {
|
|
1198
1298
|
if (running.providerTurnContentObserved && running.sessionId) {
|
|
@@ -1278,6 +1378,9 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1278
1378
|
origin,
|
|
1279
1379
|
prompt,
|
|
1280
1380
|
userMessages,
|
|
1381
|
+
...(isUserOrigin(origin) && params.actorUserId !== undefined
|
|
1382
|
+
? { actorUserId: params.actorUserId }
|
|
1383
|
+
: {}),
|
|
1281
1384
|
durableRequestIds,
|
|
1282
1385
|
attachments,
|
|
1283
1386
|
sessionId,
|
|
@@ -1329,7 +1432,9 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1329
1432
|
return rejectIsolatedTurnBeforeDispatch("failed to claim isolated turn slot");
|
|
1330
1433
|
}
|
|
1331
1434
|
if (isUserOrigin(origin)) {
|
|
1332
|
-
|
|
1435
|
+
if (remoteTurnHasSameActor(topicId, params.actorUserId, durableRequestIds)) {
|
|
1436
|
+
requestRuntimeTurnAbort(topicId, "internal");
|
|
1437
|
+
}
|
|
1333
1438
|
const queuedQueryId = waitToStartRemoteUserTurn(
|
|
1334
1439
|
{
|
|
1335
1440
|
...params,
|
|
@@ -1708,6 +1813,8 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1708
1813
|
sessionId,
|
|
1709
1814
|
userId,
|
|
1710
1815
|
actorUserId: params.actorUserId,
|
|
1816
|
+
actorTopicScope: params.actorTopicScope,
|
|
1817
|
+
remoteSession: params.remoteSession,
|
|
1711
1818
|
vaultUserId,
|
|
1712
1819
|
session: sessionName,
|
|
1713
1820
|
sessionType: sessionType ?? (isManager ? "manager" : "forum"),
|
|
@@ -1804,12 +1911,14 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1804
1911
|
"Provider session expired and conversation history could not be rebuilt; the existing session was preserved for retry.",
|
|
1805
1912
|
};
|
|
1806
1913
|
} else {
|
|
1807
|
-
|
|
1914
|
+
// No terminal event, and the same queryId carries over (see the
|
|
1915
|
+
// `_queryId` note on the empty-response retry below): this is one
|
|
1916
|
+
// logical turn recovering internally, not a turn that ended.
|
|
1808
1917
|
const retrySessionId = retry.sessionId;
|
|
1809
1918
|
logger.info(
|
|
1810
1919
|
{
|
|
1811
1920
|
topicId,
|
|
1812
|
-
|
|
1921
|
+
queryId,
|
|
1813
1922
|
agent: agentKind,
|
|
1814
1923
|
retrySessionId: retrySessionId ? retrySessionId.slice(0, 8) : null,
|
|
1815
1924
|
},
|
|
@@ -1824,6 +1933,9 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1824
1933
|
startAiTurn({
|
|
1825
1934
|
topic,
|
|
1826
1935
|
userId,
|
|
1936
|
+
actorUserId: params.actorUserId,
|
|
1937
|
+
actorTopicScope: params.actorTopicScope,
|
|
1938
|
+
remoteSession: params.remoteSession,
|
|
1827
1939
|
vaultUserId,
|
|
1828
1940
|
prompt,
|
|
1829
1941
|
_userMessages: userMessages,
|
|
@@ -1862,6 +1974,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1862
1974
|
askReplySources,
|
|
1863
1975
|
_runtimeEpoch: runtimeEpoch,
|
|
1864
1976
|
_sessionRetried: true,
|
|
1977
|
+
_queryId: queryId,
|
|
1865
1978
|
});
|
|
1866
1979
|
return;
|
|
1867
1980
|
}
|
|
@@ -1878,14 +1991,24 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1878
1991
|
// the CLI self-interrupts within ~1-2s with a zero-usage "success".
|
|
1879
1992
|
// A same-session retry has reliably produced a real reply in every
|
|
1880
1993
|
// case seen so far, so retry once before surfacing a failure.
|
|
1881
|
-
|
|
1994
|
+
// Deliberately NO terminal event here, and the retry keeps this
|
|
1995
|
+
// queryId. An internal retry is invisible bookkeeping: the turn the
|
|
1996
|
+
// caller asked for is still running. Emitting `ai_aborted` made a
|
|
1997
|
+
// host treat the turn as over — the hub revokes the turn's remote
|
|
1998
|
+
// session-comm capability on any terminal event for its queryId, so
|
|
1999
|
+
// the retried attempt found its own remote tools answering 401 —
|
|
2000
|
+
// and a fresh queryId would leave the original one with no terminal
|
|
2001
|
+
// event at all, stranding that capability for its whole ceiling.
|
|
1882
2002
|
logger.info(
|
|
1883
|
-
{ topicId,
|
|
2003
|
+
{ topicId, queryId, agent: agentKind },
|
|
1884
2004
|
"ai: retrying query after empty provider response",
|
|
1885
2005
|
);
|
|
1886
2006
|
startAiTurn({
|
|
1887
2007
|
topic,
|
|
1888
2008
|
userId,
|
|
2009
|
+
actorUserId: params.actorUserId,
|
|
2010
|
+
actorTopicScope: params.actorTopicScope,
|
|
2011
|
+
remoteSession: params.remoteSession,
|
|
1889
2012
|
vaultUserId,
|
|
1890
2013
|
prompt,
|
|
1891
2014
|
_userMessages: userMessages,
|
|
@@ -1922,6 +2045,7 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
|
|
|
1922
2045
|
_runtimeEpoch: runtimeEpoch,
|
|
1923
2046
|
_sessionRetried: sessionRetried,
|
|
1924
2047
|
_emptyResponseRetried: true,
|
|
2048
|
+
_queryId: queryId,
|
|
1925
2049
|
});
|
|
1926
2050
|
return;
|
|
1927
2051
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ActorTopicScope, RemoteSessionGrant } from "#types";
|
|
1
2
|
import type { MessageDto } from "#types/api";
|
|
2
3
|
|
|
3
4
|
export const RUNTIME_GATEWAY_VERSION = 1 as const;
|
|
@@ -114,6 +115,25 @@ export interface RuntimeGatewayTurnInput {
|
|
|
114
115
|
userId: string;
|
|
115
116
|
/** Product-side actor, when it differs from the execution principal. */
|
|
116
117
|
actorUserId?: string;
|
|
118
|
+
/**
|
|
119
|
+
* The node topic ids `actorUserId` may reach on the receiving node, as the
|
|
120
|
+
* host's membership store sees it. Omitted by hosts that predate it; the
|
|
121
|
+
* node then confines the turn's cross-room tools to the current room.
|
|
122
|
+
*/
|
|
123
|
+
actorTopicScope?: ActorTopicScope;
|
|
124
|
+
/**
|
|
125
|
+
* Hub-issued per-turn authority for remote (`node/topic`) session-comm:
|
|
126
|
+
* `{ hubUrl, capability }`. `hubUrl` must be a loopback `http://` or any
|
|
127
|
+
* `https://` origin (≤ 512 chars); `capability` an opaque `rsc1.` bearer
|
|
128
|
+
* (≤ 2 KiB) the node presents back to the hub and cannot verify — it holds
|
|
129
|
+
* no key. It does base64-decode the payload's `e` (expiry) for one purpose,
|
|
130
|
+
* ordering grants when several requests fold into one turn
|
|
131
|
+
* (`remoteSessionGrantExpiry`); that value is untrusted and takes part in no
|
|
132
|
+
* security decision. Omitted
|
|
133
|
+
* by hosts that predate it or have the feature off; the node then keeps
|
|
134
|
+
* remote session-comm fail-closed on the `otium` surface. Malformed → 400.
|
|
135
|
+
*/
|
|
136
|
+
remoteSession?: RemoteSessionGrant;
|
|
117
137
|
actorLabel?: string;
|
|
118
138
|
vaultUserId?: string;
|
|
119
139
|
/** Adapter provenance recorded on the canonical message. */
|
|
@@ -454,6 +454,9 @@ function initializeApiTopicsSchema(): void {
|
|
|
454
454
|
if (!tableColumns("api_topics").has("subagent_report_mode")) {
|
|
455
455
|
db.exec("ALTER TABLE api_topics ADD COLUMN subagent_report_mode TEXT NOT NULL DEFAULT 'auto'");
|
|
456
456
|
}
|
|
457
|
+
if (!tableColumns("api_topics").has("derived_by_user_id")) {
|
|
458
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN derived_by_user_id TEXT");
|
|
459
|
+
}
|
|
457
460
|
if (!tableColumns("api_topics").has("memory_topic_id")) {
|
|
458
461
|
db.exec("ALTER TABLE api_topics ADD COLUMN memory_topic_id TEXT");
|
|
459
462
|
}
|
|
@@ -632,6 +635,7 @@ export interface TopicRow {
|
|
|
632
635
|
is_fork: number;
|
|
633
636
|
is_subagent: number;
|
|
634
637
|
subagent_report_mode: string | null;
|
|
638
|
+
derived_by_user_id: string | null;
|
|
635
639
|
visibility: string | null;
|
|
636
640
|
surface: string | null;
|
|
637
641
|
surface_scope: string | null;
|
|
@@ -716,6 +720,7 @@ function rowToDto(
|
|
|
716
720
|
memoryTopicId: r.memory_topic_id ?? undefined,
|
|
717
721
|
memoryKey: r.memory_key ?? undefined,
|
|
718
722
|
isFork: r.is_fork !== 0,
|
|
723
|
+
...(r.derived_by_user_id ? { derivedByUserId: r.derived_by_user_id } : {}),
|
|
719
724
|
...(r.is_subagent !== 0 ? { isSubagent: true } : {}),
|
|
720
725
|
...(r.is_subagent !== 0
|
|
721
726
|
? {
|
|
@@ -859,8 +864,8 @@ export function upsertTopic(t: TopicDto): void {
|
|
|
859
864
|
`INSERT INTO api_topics
|
|
860
865
|
(id,title,kind,description,agent,base_model,base_effort,response_policy,
|
|
861
866
|
created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,
|
|
862
|
-
surface_scope,subagent_report_mode)
|
|
863
|
-
VALUES (
|
|
867
|
+
surface_scope,subagent_report_mode,derived_by_user_id)
|
|
868
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
864
869
|
ON CONFLICT(id) DO UPDATE SET
|
|
865
870
|
title = excluded.title,
|
|
866
871
|
kind = excluded.kind,
|
|
@@ -882,7 +887,10 @@ export function upsertTopic(t: TopicDto): void {
|
|
|
882
887
|
-- assignment: an update may fill in a scope that was unknown when the
|
|
883
888
|
-- room was created, but may never move a room to another workspace.
|
|
884
889
|
surface_scope = COALESCE(api_topics.surface_scope, excluded.surface_scope),
|
|
885
|
-
subagent_report_mode = excluded.subagent_report_mode
|
|
890
|
+
subagent_report_mode = excluded.subagent_report_mode,
|
|
891
|
+
-- Who asked for the derive is a fact about creation; a later update
|
|
892
|
+
-- that omits it must not erase it.
|
|
893
|
+
derived_by_user_id = COALESCE(excluded.derived_by_user_id, api_topics.derived_by_user_id)`,
|
|
886
894
|
).run(
|
|
887
895
|
t.id,
|
|
888
896
|
t.title,
|
|
@@ -907,6 +915,7 @@ export function upsertTopic(t: TopicDto): void {
|
|
|
907
915
|
normalizeTopicSurface(t.surface ?? defaultTopicSurface()),
|
|
908
916
|
surfaceScopeForWrite(t),
|
|
909
917
|
t.subagentReportMode ?? "auto",
|
|
918
|
+
t.derivedByUserId ?? null,
|
|
910
919
|
);
|
|
911
920
|
db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
|
|
912
921
|
for (const participant of t.participants) {
|
|
@@ -1305,6 +1314,16 @@ export function revokeSubagentTellTarget(subagentTopicId: string, targetTopicId:
|
|
|
1305
1314
|
* Direct children move to the deleted topic's parent (or become roots) rather
|
|
1306
1315
|
* than retaining a dangling parent id that would break memory-origin lookup.
|
|
1307
1316
|
*/
|
|
1317
|
+
/** Ids of the subagent worker rooms spawned directly from `parentTopicId`. */
|
|
1318
|
+
export function listSubagentChildTopicIds(parentTopicId: string): string[] {
|
|
1319
|
+
return db
|
|
1320
|
+
.query<{ id: string }, string>(
|
|
1321
|
+
"SELECT id FROM api_topics WHERE parent_topic_id = ? AND is_subagent = 1 ORDER BY created_at",
|
|
1322
|
+
)
|
|
1323
|
+
.all(parentTopicId)
|
|
1324
|
+
.map((row) => row.id);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1308
1327
|
export function reparentTopicChildren(
|
|
1309
1328
|
deletedTopicId: string,
|
|
1310
1329
|
replacementParentTopicId: string | null,
|