negotium 0.22.3 → 0.22.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/bin/negotium.mjs +0 -0
- package/dist/agent-helpers.js +615 -48
- package/dist/agent-helpers.js.map +11 -9
- package/dist/{chunk-6c637pwe.js → chunk-7rpbcn1j.js} +3 -3
- package/dist/{chunk-6c637pwe.js.map → chunk-7rpbcn1j.js.map} +2 -2
- package/dist/{chunk-bn1tt2qw.js → chunk-a3me2mje.js} +3 -3
- package/dist/chunk-a3me2mje.js.map +10 -0
- package/dist/{chunk-kn8y5scr.js → chunk-vh8px000.js} +12 -3
- package/dist/{chunk-kn8y5scr.js.map → chunk-vh8px000.js.map} +4 -4
- package/dist/hosted-agent.js +13 -4
- package/dist/hosted-agent.js.map +5 -5
- package/dist/main.js +1168 -264
- package/dist/main.js.map +25 -23
- package/dist/mcp-factories.js +634 -67
- package/dist/mcp-factories.js.map +11 -9
- package/dist/registry.js +3 -3
- package/dist/rollout.js +2 -2
- package/dist/runtime/src/agents/mcp-tools/request-vault.ts +616 -0
- package/dist/runtime/src/agents/rollout/claude.ts +6 -1
- package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +6 -4
- package/dist/runtime/src/application/topic-service.ts +7 -0
- package/dist/runtime/src/index.ts +9 -0
- package/dist/runtime/src/mcp-runtime-host.ts +1 -0
- package/dist/runtime/src/node-host.ts +6 -0
- package/dist/runtime/src/platform/mcp-config.ts +60 -28
- package/dist/runtime/src/runtime/turn-event-stream.ts +2 -0
- package/dist/runtime/src/runtime/turn-runner.ts +9 -4
- package/dist/runtime/src/storage/api-messages.ts +8 -2
- package/dist/runtime/src/storage/vault-request-gates.ts +359 -0
- package/dist/runtime/src/types/api.ts +23 -1
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js +4 -1
- package/dist/runtime-helpers.js.map +3 -3
- package/dist/storage.js +8 -4
- package/dist/storage.js.map +3 -3
- package/dist/types/packages/core/src/agents/mcp-tools/request-vault.d.ts +120 -0
- package/dist/types/packages/core/src/agents/rollout/claude.d.ts +5 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +8 -0
- package/dist/types/packages/core/src/runtime/turn-runner.d.ts +6 -2
- package/dist/types/packages/core/src/storage/api-messages.d.ts +1 -0
- package/dist/types/packages/core/src/storage/vault-request-gates.d.ts +69 -0
- package/dist/types/packages/core/src/types/api.d.ts +15 -1
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-bn1tt2qw.js.map +0 -10
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type SharedMcpTool } from "./common";
|
|
2
|
+
import { type RuntimeProcessLease, type RuntimeProcessLeaseHandle } from "../../storage/runtime-process-leases";
|
|
3
|
+
import { type ClaimVaultRequestGateResult, type PrepareVaultRequestGateResult, type VaultRequestGateCardUpdate } from "../../storage/vault-request-gates";
|
|
4
|
+
import type { AgentKind } from "../../types";
|
|
5
|
+
import type { MessageDto, TopicDto } from "../../types/api";
|
|
6
|
+
export interface RequestVaultToolContext {
|
|
7
|
+
userId: string;
|
|
8
|
+
topicId: string;
|
|
9
|
+
queryId?: string;
|
|
10
|
+
agent: AgentKind;
|
|
11
|
+
model?: string;
|
|
12
|
+
threadRootId?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Set when this turn's surface cannot mask text input (Telegram has no
|
|
15
|
+
* native password field, so anything a person types becomes a plaintext
|
|
16
|
+
* message that lingers in chat history). When true the tool refuses
|
|
17
|
+
* immediately and never creates a card, instead of asking for a secret on
|
|
18
|
+
* a surface that cannot protect it.
|
|
19
|
+
*/
|
|
20
|
+
unsupportedSurface?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export type AnswerVaultSecretRequestResult = {
|
|
23
|
+
ok: true;
|
|
24
|
+
queryId?: string;
|
|
25
|
+
outcome: "fulfilled" | "declined";
|
|
26
|
+
answerMessage: MessageDto;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
export interface RequestVaultRuntimeHost {
|
|
32
|
+
messaging: {
|
|
33
|
+
persistence: {
|
|
34
|
+
getMessage(topicId: string, messageId: string): MessageDto | null | undefined;
|
|
35
|
+
appendMessage(message: MessageDto): void;
|
|
36
|
+
};
|
|
37
|
+
publication: {
|
|
38
|
+
publishMessage(topicId: string, message: MessageDto): void;
|
|
39
|
+
publishCardUpdate(update: VaultRequestGateCardUpdate): void;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
topics: {
|
|
43
|
+
getTopic(topicId: string): Pick<TopicDto, "participants"> | null | undefined;
|
|
44
|
+
getConfig(topicId: string): {
|
|
45
|
+
model?: string;
|
|
46
|
+
} | undefined;
|
|
47
|
+
};
|
|
48
|
+
vault: {
|
|
49
|
+
hasKey(userId: string, key: string): boolean;
|
|
50
|
+
/** Writes the secret. Called before the gate is claimed, so a storage
|
|
51
|
+
* failure never leaves a card marked "fulfilled" without a stored value. */
|
|
52
|
+
set(userId: string, key: string, value: string, description: string): void;
|
|
53
|
+
};
|
|
54
|
+
durability: {
|
|
55
|
+
gates: {
|
|
56
|
+
prepare(args: {
|
|
57
|
+
gateId: string;
|
|
58
|
+
topicId: string;
|
|
59
|
+
queryId?: string;
|
|
60
|
+
idempotencyKey: string;
|
|
61
|
+
bodyHash: string;
|
|
62
|
+
messageId: string;
|
|
63
|
+
ownerId: string;
|
|
64
|
+
now: string;
|
|
65
|
+
}): PrepareVaultRequestGateResult;
|
|
66
|
+
claimAndAnswer(args: {
|
|
67
|
+
topicId: string;
|
|
68
|
+
messageId: string;
|
|
69
|
+
outcome: "fulfilled" | "declined";
|
|
70
|
+
userId: string;
|
|
71
|
+
ownerId: string;
|
|
72
|
+
source: string;
|
|
73
|
+
now: string;
|
|
74
|
+
}): ClaimVaultRequestGateResult;
|
|
75
|
+
cancel(topicId: string, messageId: string, ownerId: string, now: string): VaultRequestGateCardUpdate | null;
|
|
76
|
+
quarantine(gateId: string, ownerId: string, now: string): VaultRequestGateCardUpdate | null;
|
|
77
|
+
quarantineForeign(liveOwnerIds: ReadonlySet<string>, now: string): VaultRequestGateCardUpdate[];
|
|
78
|
+
};
|
|
79
|
+
processLeases: {
|
|
80
|
+
acquire(role: string, ownerId: string): RuntimeProcessLeaseHandle | null;
|
|
81
|
+
list(rolePrefix: string): RuntimeProcessLease[];
|
|
82
|
+
isAlive(lease: RuntimeProcessLease): boolean;
|
|
83
|
+
removeDead(rolePrefix: string): void;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
runtime: {
|
|
87
|
+
ownerId: string;
|
|
88
|
+
createId(): string;
|
|
89
|
+
now(): string;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export interface RequestVaultRuntime {
|
|
93
|
+
startGateOwner(): void;
|
|
94
|
+
stopGateOwner(): void;
|
|
95
|
+
reconcilePendingGates(): number;
|
|
96
|
+
answerPendingRequest(topicId: string, messageId: string, userId: string, value: string | null): AnswerVaultSecretRequestResult;
|
|
97
|
+
cancelPendingRequests(topicId: string, queryId: string): void;
|
|
98
|
+
createToolDefinition(ctx: RequestVaultToolContext): SharedMcpTool;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create an isolated request-vault-secret lifecycle over host-provided
|
|
102
|
+
* messaging, topic, vault, durability, and runtime services. Mirrors
|
|
103
|
+
* `createAskUserRuntime`'s durable-gate shape, but the thing being
|
|
104
|
+
* exchanged is "did the user supply a value", never the value itself.
|
|
105
|
+
*/
|
|
106
|
+
export declare function createRequestVaultRuntime(host: RequestVaultRuntimeHost): RequestVaultRuntime;
|
|
107
|
+
export declare const defaultRequestVaultDurabilityHost: RequestVaultRuntimeHost["durability"];
|
|
108
|
+
export declare function startRequestVaultGateOwner(): void;
|
|
109
|
+
export declare function stopRequestVaultGateOwner(): void;
|
|
110
|
+
export declare function reconcilePendingRequestVaultGates(): number;
|
|
111
|
+
/**
|
|
112
|
+
* Host-neutral answer path for pending vault-request cards. Terminal and
|
|
113
|
+
* Otium should call this rather than reimplementing pending-map and
|
|
114
|
+
* persistence ordering. `value: null` means the user declined; any other
|
|
115
|
+
* string is the secret, stored via `vaultSet` before the gate is claimed,
|
|
116
|
+
* and never echoed back in the result.
|
|
117
|
+
*/
|
|
118
|
+
export declare function answerPendingVaultSecretRequest(topicId: string, messageId: string, userId: string, value: string | null): AnswerVaultSecretRequestResult;
|
|
119
|
+
export declare function cancelPendingVaultSecretRequests(topicId: string, queryId: string): void;
|
|
120
|
+
export declare function createRequestVaultToolDefinition(ctx: RequestVaultToolContext): SharedMcpTool;
|
|
@@ -26,6 +26,11 @@ import type { ConversationEntry } from "../../storage/conversations";
|
|
|
26
26
|
* synthetic rollouts landed at `…-user_6407801418/` while the SDK looked
|
|
27
27
|
* at `…-user-6407801418/` — every `set_agent` switch surfaced as "No
|
|
28
28
|
* conversation found with session ID" on the next message.
|
|
29
|
+
*
|
|
30
|
+
* Nothing is prepended: a POSIX path starts with `/`, which already becomes
|
|
31
|
+
* the leading `-`. A Windows path does not, and the SDK writes
|
|
32
|
+
* `C:\Users\x` as `C--Users-x`; forcing a `-` in front sent Windows rollouts
|
|
33
|
+
* to `-C--Users-x/`, where the SDK never looks.
|
|
29
34
|
*/
|
|
30
35
|
export declare function encodeClaudeCwd(cwd: string): string;
|
|
31
36
|
export interface ClaudeRolloutOptions {
|
|
@@ -2,6 +2,7 @@ import { type RuntimeMcpScope } from "./mcp-catalog-policy";
|
|
|
2
2
|
import { browserOwnerCapability } from "./playwright/capability";
|
|
3
3
|
import { buildPlaywrightMcpTransport, CODEX_BROWSER_CAPABILITY_ENV } from "./playwright/mcp-transport";
|
|
4
4
|
import type { ActorTopicScope, AgentKind, AgentQueryOptions, PeerRuntimeBridgeContext, RemoteSessionGrant } from "../types";
|
|
5
|
+
import type { TopicDto } from "../types/api";
|
|
5
6
|
export type { RuntimeMcpScope } from "./mcp-catalog-policy";
|
|
6
7
|
/**
|
|
7
8
|
* Build the stdio transport spec for a Otium MCP server entry.
|
|
@@ -172,6 +173,13 @@ export declare function setNodeMcpServers(entries: NodeMcpEntry[]): void;
|
|
|
172
173
|
export declare function getNodeMcpServers(): readonly NodeMcpEntry[];
|
|
173
174
|
/** Prevent host grants from replacing node-owned MCP servers. */
|
|
174
175
|
export declare function isReservedRuntimeMcpServerName(name: string): boolean;
|
|
176
|
+
/**
|
|
177
|
+
* Why `topic` may not receive host MCP servers named `names`, or null when it
|
|
178
|
+
* may. A manager room takes any of them. An ordinary room takes everything but
|
|
179
|
+
* the manager administration surface, so a host can scope its own tools to a
|
|
180
|
+
* room. A subagent room takes none.
|
|
181
|
+
*/
|
|
182
|
+
export declare function hostMcpGrantRefusal(topic: Pick<TopicDto, "kind" | "isSubagent">, names: readonly string[]): string | null;
|
|
175
183
|
/**
|
|
176
184
|
* Lazily prepare per-topic HTTP manifest entries before a provider resolves
|
|
177
185
|
* its otherwise-synchronous MCP config. One failed custom server is omitted
|
|
@@ -186,8 +186,12 @@ export declare function resolveTurnToolCapabilities(topicId: string, params: {
|
|
|
186
186
|
visualTools: boolean;
|
|
187
187
|
fileDeliveryTools: boolean;
|
|
188
188
|
};
|
|
189
|
-
/**
|
|
190
|
-
|
|
189
|
+
/**
|
|
190
|
+
* A manager room's grant reaches only manager executions and an ordinary
|
|
191
|
+
* room's only forum executions, so cron/ephemeral turns cannot inherit it.
|
|
192
|
+
* Subagent rooms never take one.
|
|
193
|
+
*/
|
|
194
|
+
export declare function resolveTurnHostMcpServers(topic: Pick<TopicDto, "id" | "kind" | "isSubagent">, sessionType?: "dm" | "forum" | "ephemeral" | "manager" | "cron"): Record<string, import("../runtime-gateway").HostMcpServerSpec> | undefined;
|
|
191
195
|
export interface StartAiTurnParams extends AiTurnExecutionOptions {
|
|
192
196
|
topic: AiTurnTopic;
|
|
193
197
|
userId: string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { VaultSecretRequestDto } from "../types/api";
|
|
2
|
+
export type VaultRequestGateState = "pending" | "claimed" | "answered" | "cancelled" | "quarantined";
|
|
3
|
+
export interface VaultRequestGateRecord {
|
|
4
|
+
gateId: string;
|
|
5
|
+
topicId: string;
|
|
6
|
+
queryId?: string;
|
|
7
|
+
idempotencyKey: string;
|
|
8
|
+
bodyHash: string;
|
|
9
|
+
messageId: string;
|
|
10
|
+
ownerId: string;
|
|
11
|
+
state: VaultRequestGateState;
|
|
12
|
+
outcome?: "fulfilled" | "declined";
|
|
13
|
+
answeredBy?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface VaultRequestGateCardUpdate {
|
|
16
|
+
topicId: string;
|
|
17
|
+
messageId: string;
|
|
18
|
+
vaultSecretRequest: VaultSecretRequestDto;
|
|
19
|
+
editedAt: string;
|
|
20
|
+
}
|
|
21
|
+
export type PrepareVaultRequestGateResult = {
|
|
22
|
+
outcome: "created";
|
|
23
|
+
gate: VaultRequestGateRecord;
|
|
24
|
+
} | {
|
|
25
|
+
outcome: "pending";
|
|
26
|
+
gate: VaultRequestGateRecord;
|
|
27
|
+
} | {
|
|
28
|
+
outcome: "replay";
|
|
29
|
+
gate: VaultRequestGateRecord;
|
|
30
|
+
} | {
|
|
31
|
+
outcome: "conflict";
|
|
32
|
+
gate: VaultRequestGateRecord;
|
|
33
|
+
};
|
|
34
|
+
export declare function prepareVaultRequestGate(args: {
|
|
35
|
+
gateId: string;
|
|
36
|
+
topicId: string;
|
|
37
|
+
queryId?: string;
|
|
38
|
+
idempotencyKey: string;
|
|
39
|
+
bodyHash: string;
|
|
40
|
+
messageId: string;
|
|
41
|
+
ownerId: string;
|
|
42
|
+
now: string;
|
|
43
|
+
}): PrepareVaultRequestGateResult;
|
|
44
|
+
export declare function quarantineVaultRequestGate(gateId: string, ownerId: string, now?: string): VaultRequestGateCardUpdate | null;
|
|
45
|
+
export declare function quarantineForeignVaultRequestGates(liveOwnerIds: ReadonlySet<string>, now?: string): VaultRequestGateCardUpdate[];
|
|
46
|
+
export type ClaimVaultRequestGateResult = {
|
|
47
|
+
outcome: "claimed";
|
|
48
|
+
gate: VaultRequestGateRecord;
|
|
49
|
+
vaultSecretRequest: VaultSecretRequestDto;
|
|
50
|
+
editedAt: string;
|
|
51
|
+
} | {
|
|
52
|
+
outcome: "unavailable";
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Claim the gate and mark the card's outcome. This never touches the secret
|
|
56
|
+
* value itself — the caller is responsible for writing to the vault (via
|
|
57
|
+
* `vaultSet`) *before* calling this, so the card only ever reflects
|
|
58
|
+
* "fulfilled"/"declined", never the plaintext.
|
|
59
|
+
*/
|
|
60
|
+
export declare function claimVaultRequestGateAndAnswer(args: {
|
|
61
|
+
topicId: string;
|
|
62
|
+
messageId: string;
|
|
63
|
+
outcome: "fulfilled" | "declined";
|
|
64
|
+
userId: string;
|
|
65
|
+
ownerId: string;
|
|
66
|
+
source: string;
|
|
67
|
+
now: string;
|
|
68
|
+
}): ClaimVaultRequestGateResult;
|
|
69
|
+
export declare function cancelVaultRequestGate(topicId: string, messageId: string, ownerId: string, now?: string): VaultRequestGateCardUpdate | null;
|
|
@@ -249,8 +249,9 @@ export interface MessageDto {
|
|
|
249
249
|
threadLastReplyAt?: string;
|
|
250
250
|
/** Emoji reactions, one entry per (user, emoji). */
|
|
251
251
|
reactions?: ReactionDto[];
|
|
252
|
-
kind?: "message" | "system" | "tool" | "ask_user_question" | "subagent" | "tell";
|
|
252
|
+
kind?: "message" | "system" | "tool" | "ask_user_question" | "vault_secret_request" | "subagent" | "tell";
|
|
253
253
|
askUserQuestion?: AskUserQuestionDto;
|
|
254
|
+
vaultSecretRequest?: VaultSecretRequestDto;
|
|
254
255
|
subagentCard?: SubagentCardDto;
|
|
255
256
|
tellCard?: TellCardDto;
|
|
256
257
|
createdAt: string;
|
|
@@ -264,6 +265,18 @@ export interface AskUserQuestionDto {
|
|
|
264
265
|
selectedLabel?: string;
|
|
265
266
|
expired?: boolean;
|
|
266
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Blocking card for `request_vault_secret`: the agent proposes a key and
|
|
270
|
+
* description, a person types only the value on a surface that can mask
|
|
271
|
+
* input, and the plaintext never flows back through this DTO, a tool
|
|
272
|
+
* result, or chat history — only the outcome does.
|
|
273
|
+
*/
|
|
274
|
+
export interface VaultSecretRequestDto {
|
|
275
|
+
key: string;
|
|
276
|
+
description?: string;
|
|
277
|
+
status: "pending" | "fulfilled" | "declined" | "expired";
|
|
278
|
+
answeredBy?: string;
|
|
279
|
+
}
|
|
267
280
|
/** Structured cross-session delivery. Never inferred from system-message text. */
|
|
268
281
|
export interface TellCardDto {
|
|
269
282
|
fromLabel: string;
|
|
@@ -461,6 +474,7 @@ export type WsServerMessage = {
|
|
|
461
474
|
editedAt?: string;
|
|
462
475
|
usage?: MessageTokenUsage;
|
|
463
476
|
askUserQuestion?: AskUserQuestionDto;
|
|
477
|
+
vaultSecretRequest?: VaultSecretRequestDto;
|
|
464
478
|
subagentCard?: SubagentCardDto;
|
|
465
479
|
} | {
|
|
466
480
|
type: "reaction_update";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const NEGOTIUM_VERSION = "0.22.
|
|
1
|
+
export declare const NEGOTIUM_VERSION = "0.22.5";
|
package/package.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../packages/core/src/agents/rollout/claude.ts"],
|
|
4
|
-
"sourcesContent": [
|
|
5
|
-
"/**\n * Claude rollout encoder.\n *\n * Materializes a synthetic JSONL at `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`\n * so a subsequent `claude.query({ resume: sessionId })` continues the\n * cross-agent conversation as if it had always been native. Verified by\n * sandbox PoCs (test-7c/test-7d): the SDK accepts these without checksum\n * gating, requiring only `user`/`assistant` messages and a small per-turn\n * attachment chain captured into `agents/fixtures/claude-attachments.jsonl`.\n *\n * Tool history is folded into assistant text as `[Tool: ...]` annotations\n * (see shared.ts:extractChatPairs) — synthetic tool_use IDs across SDKs are\n * too fragile to reconstruct structurally.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n assertUuidLike,\n type ChatPair,\n clone,\n ensureCwdExists,\n extractChatPairs,\n FIXTURES_DIR,\n} from \"#agents/rollout/shared\";\nimport { parseJsonlText, writeJsonlFile } from \"#platform/jsonl\";\nimport { logger } from \"#platform/logger\";\nimport type { ConversationEntry } from \"#storage/conversations\";\n\ninterface ClaudeAttachments {\n deferredToolsDelta: Record<string, unknown>;\n skillListing: Record<string, unknown>;\n}\n\nlet _attachmentsCache: ClaudeAttachments | null = null;\nfunction loadClaudeAttachments(): ClaudeAttachments {\n if (_attachmentsCache) return _attachmentsCache;\n const raw = readFileSync(join(FIXTURES_DIR, \"claude-attachments.jsonl\"), \"utf8\");\n const lines = parseJsonlText<Record<string, unknown>>(raw);\n if (lines.length < 2) {\n throw new Error(\n `loadClaudeAttachments: expected >=2 entries in claude-attachments.jsonl, got ${lines.length}`,\n );\n }\n _attachmentsCache = {\n deferredToolsDelta: lines[0],\n skillListing: lines[1],\n };\n return _attachmentsCache;\n}\n\n/**\n * Version strings stamped onto synthetic rollouts so they look indistinguishable\n * from a live SDK session. These do not affect resume behavior — the SDKs do\n * not gate on them — but keeping them current avoids confusion in observability\n * tooling that scans rollout files.\n */\nconst CLAUDE_SDK_VERSION = \"2.1.126\";\nconst CLAUDE_DEFAULT_MODEL = \"claude-sonnet-5\";\nconst CLAUDE_DEFAULT_GIT_BRANCH = \"HEAD\";\n\n/**\n * Encode a directory path the way Claude SDK expects in\n * `~/.claude/projects/<encoded-cwd>/`. macOS resolves `/tmp` to\n * `/private/tmp`, mirroring what the SDK records.\n *\n * The SDK normalizes ANY non-alphanumeric character to `-` (verified\n * empirically on disk: `/Users/maestrobot/.../user_6407801418` →\n * `-Users-...-user-6407801418`, i.e. both `/` AND `_` collapse to `-`).\n * A previous implementation only replaced `/` and left `_` intact, so\n * synthetic rollouts landed at `…-user_6407801418/` while the SDK looked\n * at `…-user-6407801418/` — every `set_agent` switch surfaced as \"No\n * conversation found with session ID\" on the next message.\n */\nexport function encodeClaudeCwd(cwd: string): string {\n const realCwd = cwd.startsWith(\"/tmp/\") ? cwd.replace(/^\\/tmp\\//, \"/private/tmp/\") : cwd;\n return `-${realCwd.replaceAll(/[^a-zA-Z0-9]/g, \"-\").replace(/^-/, \"\")}`;\n}\n\nexport interface ClaudeRolloutOptions {\n /** Working directory the resumed Claude session will report. */\n cwd: string;\n /** Optional override; default = freshly generated UUIDv4. */\n sessionId?: string;\n /**\n * Pairs to encode. If omitted, derived from `entries` via extractChatPairs.\n * Pass explicit pairs when the caller has already shaped the dialogue.\n */\n pairs?: ChatPair[];\n /** When `pairs` is omitted, the source UnifiedEvent log to digest. */\n entries?: ConversationEntry[];\n /** Full provider model id stamped on synthesized assistant history. */\n model?: string;\n}\n\nexport interface ClaudeRolloutResult {\n sessionId: string;\n rolloutPath: string;\n}\n\n/**\n * Materialize a Claude rollout JSONL with the provided dialogue and place it\n * at `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl` so a subsequent\n * `claude.query({ resume: sessionId })` will continue the conversation.\n */\nexport function writeClaudeRollout(opts: ClaudeRolloutOptions): ClaudeRolloutResult {\n const sessionId = opts.sessionId ?? randomUUID();\n assertUuidLike(\"sessionId\", sessionId);\n ensureCwdExists(opts.cwd);\n const pairs = opts.pairs ?? extractChatPairs(opts.entries ?? []);\n const cwdReal = opts.cwd.startsWith(\"/tmp/\")\n ? opts.cwd.replace(/^\\/tmp\\//, \"/private/tmp/\")\n : opts.cwd;\n\n const lines: unknown[] = [];\n const ts = () => new Date().toISOString();\n let lastUuid: string | null = null;\n\n // Queue-operation noise that the live SDK records around each turn. Not\n // strictly required for resume but mirrors what the SDK itself writes,\n // keeping the synthetic file indistinguishable.\n lines.push({ type: \"queue-operation\", operation: \"enqueue\", timestamp: ts(), sessionId });\n lines.push({ type: \"queue-operation\", operation: \"dequeue\", timestamp: ts(), sessionId });\n\n // Per-turn attachment chain captured from a real Claude rollout. claude SDK\n // emits `deferred_tools_delta` followed by `skill_listing` immediately after\n // every user message, building a parentUuid chain user → att1 → att2 →\n // assistant. PoC-7d preserved this shape; replaying it keeps the resumed\n // session indistinguishable from a native one.\n const attachments = loadClaudeAttachments();\n\n for (const pair of pairs) {\n const userUuid = randomUUID();\n lines.push({\n parentUuid: lastUuid,\n isSidechain: false,\n promptId: randomUUID(),\n type: \"user\",\n message: { role: \"user\", content: [{ type: \"text\", text: pair.userText }] },\n uuid: userUuid,\n timestamp: ts(),\n permissionMode: \"bypassPermissions\",\n userType: \"external\",\n entrypoint: \"sdk-ts\",\n cwd: cwdReal,\n sessionId,\n version: CLAUDE_SDK_VERSION,\n gitBranch: CLAUDE_DEFAULT_GIT_BRANCH,\n });\n\n // Attachment 1: deferred_tools_delta — chain from user.\n const att1Uuid = randomUUID();\n const att1 = clone(attachments.deferredToolsDelta);\n (att1 as Record<string, unknown>).parentUuid = userUuid;\n (att1 as Record<string, unknown>).uuid = att1Uuid;\n (att1 as Record<string, unknown>).timestamp = ts();\n (att1 as Record<string, unknown>).sessionId = sessionId;\n (att1 as Record<string, unknown>).cwd = cwdReal;\n lines.push(att1);\n\n // Attachment 2: skill_listing — chain from att1.\n const att2Uuid = randomUUID();\n const att2 = clone(attachments.skillListing);\n (att2 as Record<string, unknown>).parentUuid = att1Uuid;\n (att2 as Record<string, unknown>).uuid = att2Uuid;\n (att2 as Record<string, unknown>).timestamp = ts();\n (att2 as Record<string, unknown>).sessionId = sessionId;\n (att2 as Record<string, unknown>).cwd = cwdReal;\n lines.push(att2);\n\n const assistantUuid = randomUUID();\n lines.push({\n parentUuid: att2Uuid,\n isSidechain: false,\n message: {\n model: opts.model ?? CLAUDE_DEFAULT_MODEL,\n id: `msg_${randomUUID().replace(/-/g, \"\").slice(0, 24)}`,\n type: \"message\",\n role: \"assistant\",\n content: [{ type: \"text\", text: pair.assistantText }],\n stop_reason: \"end_turn\",\n stop_sequence: null,\n stop_details: null,\n usage: {\n input_tokens: 0,\n cache_creation_input_tokens: 0,\n cache_read_input_tokens: 0,\n output_tokens: 0,\n server_tool_use: { web_search_requests: 0, web_fetch_requests: 0 },\n service_tier: \"standard\",\n cache_creation: { ephemeral_1h_input_tokens: 0, ephemeral_5m_input_tokens: 0 },\n inference_geo: \"\",\n iterations: [],\n speed: \"standard\",\n },\n diagnostics: null,\n },\n requestId: `req_${randomUUID().replace(/-/g, \"\").slice(0, 22)}`,\n type: \"assistant\",\n uuid: assistantUuid,\n timestamp: ts(),\n userType: \"external\",\n entrypoint: \"sdk-ts\",\n cwd: cwdReal,\n sessionId,\n version: CLAUDE_SDK_VERSION,\n gitBranch: CLAUDE_DEFAULT_GIT_BRANCH,\n });\n lastUuid = assistantUuid;\n }\n\n const projectsDir = join(homedir(), \".claude\", \"projects\", encodeClaudeCwd(opts.cwd));\n const path = join(projectsDir, `${sessionId}.jsonl`);\n writeJsonlFile(path, lines);\n logger.info(\n { sessionId, path, pairs: pairs.length },\n \"writeClaudeRollout: synthetic rollout placed\",\n );\n return { sessionId, rolloutPath: path };\n}\n\n/**\n * Remove the latest poisoned thinking turn and everything after it from the\n * session JSONL.\n *\n * When a turn ends with stop_reason=\"max_tokens\" while extended thinking is\n * active, the Claude CLI writes the truncated thinking block verbatim to the\n * rollout file. The Anthropic API rejects any subsequent resume that replays\n * this incomplete block with 400 \"thinking/redacted_thinking blocks cannot be\n * modified\". Dropping the entire poisoned turn (user entry + attachments +\n * truncated assistant) leaves the JSONL in a clean state so the next\n * claude.query({ resume }) can proceed without a 400.\n *\n * Returns true when the JSONL was found and repaired, false otherwise\n * (file missing, no poisoned assistant turn found, I/O error). Error-path\n * callers fall back to clearing the session ID when false is returned.\n */\nexport function repairPoisonedRollout(sessionId: string, cwd: string): boolean {\n try {\n const path = join(homedir(), \".claude\", \"projects\", encodeClaudeCwd(cwd), `${sessionId}.jsonl`);\n if (!existsSync(path)) return false;\n\n const lines = readFileSync(path, \"utf8\")\n .split(\"\\n\")\n .filter((l) => l.trim());\n\n // Walk backwards to find the latest assistant turn that actually contains\n // the poisoned shape: max_tokens plus a thinking/redacted_thinking block.\n // A resume failure can append a fresh user entry after the poisoned turn;\n // trimming the last user would remove only the retry and leave the bad\n // assistant in place.\n let poisonedAssistantIdx = -1;\n for (let i = lines.length - 1; i >= 0; i--) {\n const entry = parseClaudeRolloutLine(lines[i]);\n if (entry && isPoisonedAssistantEntry(entry)) {\n poisonedAssistantIdx = i;\n break;\n }\n }\n\n if (poisonedAssistantIdx < 0) {\n logger.warn({ sessionId, path }, \"repairPoisonedRollout: no poisoned assistant entry found\");\n return false;\n }\n\n let turnStartIdx = -1;\n for (let i = poisonedAssistantIdx - 1; i >= 0; i--) {\n const entry = parseClaudeRolloutLine(lines[i]);\n if (entry?.type === \"user\") {\n turnStartIdx = i;\n break;\n }\n }\n\n if (turnStartIdx < 0) {\n logger.warn(\n { sessionId, path, poisonedAssistantIdx },\n \"repairPoisonedRollout: poisoned assistant has no preceding user entry\",\n );\n return false;\n }\n\n const kept = lines.slice(0, turnStartIdx);\n writeFileSync(path, kept.length ? `${kept.join(\"\\n\")}\\n` : \"\");\n logger.warn(\n {\n sessionId,\n path,\n poisonedAssistantIdx,\n turnStartIdx,\n droppedEntries: lines.length - turnStartIdx,\n keptEntries: kept.length,\n },\n \"repairPoisonedRollout: truncated turn removed from rollout JSONL\",\n );\n return true;\n } catch (err) {\n logger.warn({ err, sessionId, cwd }, \"repairPoisonedRollout: I/O error\");\n return false;\n }\n}\n\ntype ClaudeRolloutEntry = {\n type?: string;\n message?: {\n stop_reason?: string | null;\n content?: unknown;\n };\n};\n\nfunction parseClaudeRolloutLine(line: string): ClaudeRolloutEntry | null {\n try {\n return JSON.parse(line) as ClaudeRolloutEntry;\n } catch {\n return null;\n }\n}\n\nfunction isPoisonedAssistantEntry(entry: ClaudeRolloutEntry): boolean {\n if (entry.type !== \"assistant\") return false;\n if (entry.message?.stop_reason !== \"max_tokens\") return false;\n const content = entry.message.content;\n if (!Array.isArray(content)) return false;\n return content.some((block) => {\n if (!block || typeof block !== \"object\") return false;\n const type = (block as { type?: unknown }).type;\n return type === \"thinking\" || type === \"redacted_thinking\";\n });\n}\n"
|
|
6
|
-
],
|
|
7
|
-
"mappings": "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",
|
|
8
|
-
"debugId": "1ECE58AFF7B086BE64756E2164756E21",
|
|
9
|
-
"names": []
|
|
10
|
-
}
|