negotium 0.1.6 → 0.1.8
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/README.md +2 -2
- package/dist/chunk-0ay6cjdy.js +1331 -0
- package/dist/chunk-0ay6cjdy.js.map +16 -0
- package/dist/chunk-1k7t7bnx.js +97 -0
- package/dist/chunk-1k7t7bnx.js.map +10 -0
- package/dist/chunk-5ahkew9c.js +806 -0
- package/dist/chunk-5ahkew9c.js.map +17 -0
- package/dist/chunk-6cpn7ht2.js +287 -0
- package/dist/chunk-6cpn7ht2.js.map +13 -0
- package/dist/chunk-6kctn20e.js +3529 -0
- package/dist/chunk-6kctn20e.js.map +20 -0
- package/dist/chunk-7xqj7aex.js +77 -0
- package/dist/chunk-7xqj7aex.js.map +11 -0
- package/dist/chunk-9p0ww3tk.js +41 -0
- package/dist/chunk-9p0ww3tk.js.map +10 -0
- package/dist/chunk-a92a7nrk.js +252 -0
- package/dist/chunk-a92a7nrk.js.map +10 -0
- package/dist/chunk-c500xe9z.js +33 -0
- package/dist/chunk-c500xe9z.js.map +10 -0
- package/dist/chunk-cds1qxsf.js +1644 -0
- package/dist/chunk-cds1qxsf.js.map +17 -0
- package/dist/chunk-e4407g8x.js +274 -0
- package/dist/chunk-e4407g8x.js.map +10 -0
- package/dist/chunk-e5pbggsh.js +363 -0
- package/dist/chunk-e5pbggsh.js.map +11 -0
- package/dist/chunk-edb07sjp.js +567 -0
- package/dist/chunk-edb07sjp.js.map +14 -0
- package/dist/chunk-exd0v74a.js +3012 -0
- package/dist/chunk-exd0v74a.js.map +32 -0
- package/dist/chunk-fgrme9tr.js +4398 -0
- package/dist/chunk-fgrme9tr.js.map +47 -0
- package/dist/chunk-fka9eqe1.js +581 -0
- package/dist/chunk-fka9eqe1.js.map +11 -0
- package/dist/chunk-g82jygez.js +108 -0
- package/dist/chunk-g82jygez.js.map +10 -0
- package/dist/chunk-ghs2j1fr.js +312 -0
- package/dist/chunk-ghs2j1fr.js.map +10 -0
- package/dist/chunk-gmy11efb.js +125 -0
- package/dist/chunk-gmy11efb.js.map +10 -0
- package/dist/chunk-gxma4c70.js +110 -0
- package/dist/chunk-gxma4c70.js.map +10 -0
- package/dist/chunk-hkdqhm34.js +61 -0
- package/dist/chunk-hkdqhm34.js.map +10 -0
- package/dist/chunk-hpa0cpzh.js +134 -0
- package/dist/chunk-hpa0cpzh.js.map +10 -0
- package/dist/chunk-hwy3n31w.js +21 -0
- package/dist/chunk-hwy3n31w.js.map +9 -0
- package/dist/chunk-j2behpy7.js +214 -0
- package/dist/chunk-j2behpy7.js.map +10 -0
- package/dist/chunk-jc0at0tc.js +102 -0
- package/dist/chunk-jc0at0tc.js.map +10 -0
- package/dist/chunk-jkqq5ef6.js +243 -0
- package/dist/chunk-jkqq5ef6.js.map +11 -0
- package/dist/chunk-jzkw2f01.js +51 -0
- package/dist/chunk-jzkw2f01.js.map +10 -0
- package/dist/chunk-jzz91jwh.js +70 -0
- package/dist/chunk-jzz91jwh.js.map +10 -0
- package/dist/chunk-k9z47mab.js +247 -0
- package/dist/chunk-k9z47mab.js.map +12 -0
- package/dist/chunk-n17yxpa8.js +23 -0
- package/dist/chunk-n17yxpa8.js.map +10 -0
- package/dist/chunk-naa09ags.js +2143 -0
- package/dist/chunk-naa09ags.js.map +15 -0
- package/dist/chunk-ns3ke88n.js +142 -0
- package/dist/chunk-ns3ke88n.js.map +10 -0
- package/dist/chunk-p3cr2ejg.js +1626 -0
- package/dist/chunk-p3cr2ejg.js.map +15 -0
- package/dist/chunk-q5bcpf23.js +2925 -0
- package/dist/chunk-q5bcpf23.js.map +20 -0
- package/dist/chunk-rafc55bs.js +424 -0
- package/dist/chunk-rafc55bs.js.map +11 -0
- package/dist/chunk-rbrxjvbj.js +2830 -0
- package/dist/chunk-rbrxjvbj.js.map +23 -0
- package/dist/chunk-sa874j5c.js +9 -0
- package/dist/chunk-sa874j5c.js.map +10 -0
- package/dist/chunk-sgsz2fpq.js +289 -0
- package/dist/chunk-sgsz2fpq.js.map +10 -0
- package/dist/chunk-sm6fh5c5.js +569 -0
- package/dist/chunk-sm6fh5c5.js.map +14 -0
- package/dist/chunk-t9zcg8f1.js +499 -0
- package/dist/chunk-t9zcg8f1.js.map +13 -0
- package/dist/chunk-vp2db42d.js +121 -0
- package/dist/chunk-vp2db42d.js.map +10 -0
- package/dist/chunk-w13jjf4a.js +46 -0
- package/dist/chunk-w13jjf4a.js.map +10 -0
- package/dist/chunk-x1arr7ze.js +63 -0
- package/dist/chunk-x1arr7ze.js.map +10 -0
- package/dist/chunk-xfq68rch.js +583 -0
- package/dist/chunk-xfq68rch.js.map +11 -0
- package/dist/main.js +132 -20
- package/dist/main.js.map +4 -4
- package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
- package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
- package/dist/runtime/src/agents/archiver.ts +396 -0
- package/dist/runtime/src/agents/auth-check.ts +64 -0
- package/dist/runtime/src/agents/claude-provider.ts +737 -0
- package/dist/runtime/src/agents/claude-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
- package/dist/runtime/src/agents/codex-provider.ts +785 -0
- package/dist/runtime/src/agents/codex-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
- package/dist/runtime/src/agents/contracts.ts +69 -0
- package/dist/runtime/src/agents/deep-map.ts +13 -0
- package/dist/runtime/src/agents/execution-host.ts +91 -0
- package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
- package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
- package/dist/runtime/src/agents/fork.ts +65 -0
- package/dist/runtime/src/agents/hosted-agent.ts +48 -0
- package/dist/runtime/src/agents/idle-archiver.ts +128 -0
- package/dist/runtime/src/agents/index.ts +205 -0
- package/dist/runtime/src/agents/maestro-provider.ts +206 -0
- package/dist/runtime/src/agents/maestro-registry.ts +31 -0
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
- package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
- package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
- package/dist/runtime/src/agents/model-catalog.ts +151 -0
- package/dist/runtime/src/agents/registry.ts +24 -0
- package/dist/runtime/src/agents/rollout/claude.ts +331 -0
- package/dist/runtime/src/agents/rollout/codex.ts +569 -0
- package/dist/runtime/src/agents/rollout/shared.ts +205 -0
- package/dist/runtime/src/agents/self-config-core.ts +415 -0
- package/dist/runtime/src/agents/task-events.ts +44 -0
- package/dist/runtime/src/agents/tool-format.ts +352 -0
- package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
- package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
- package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
- package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
- package/dist/runtime/src/application/submit-user-message.ts +47 -0
- package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
- package/dist/runtime/src/application/switch-topic-model.ts +79 -0
- package/dist/runtime/src/application/topic-service.ts +139 -0
- package/dist/runtime/src/application/vault-command.ts +170 -0
- package/dist/runtime/src/bus.ts +319 -0
- package/dist/runtime/src/index.ts +333 -0
- package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
- package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
- package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
- package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
- package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
- package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
- package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +134 -0
- package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
- package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
- package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
- package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
- package/dist/runtime/src/mcp/system-health-server.ts +130 -0
- package/dist/runtime/src/mcp/task-server.ts +173 -0
- package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
- package/dist/runtime/src/mcp/vault-http.ts +219 -0
- package/dist/runtime/src/mcp/vault-run.ts +153 -0
- package/dist/runtime/src/mcp/vault-server.ts +104 -0
- package/dist/runtime/src/mcp/wiki-server.ts +755 -0
- package/dist/runtime/src/media/file-events.ts +33 -0
- package/dist/runtime/src/media/text-extractor.ts +312 -0
- package/dist/runtime/src/media/video.ts +92 -0
- package/dist/runtime/src/outbox/file-ops.ts +210 -0
- package/dist/runtime/src/outbox/index.ts +8 -0
- package/dist/runtime/src/outbox/utils.ts +51 -0
- package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
- package/dist/runtime/src/platform/config.ts +371 -0
- package/dist/runtime/src/platform/constants.ts +18 -0
- package/dist/runtime/src/platform/debug-users.ts +14 -0
- package/dist/runtime/src/platform/delay.ts +1 -0
- package/dist/runtime/src/platform/error.ts +9 -0
- package/dist/runtime/src/platform/file-utils.ts +16 -0
- package/dist/runtime/src/platform/jsonl.ts +202 -0
- package/dist/runtime/src/platform/lifecycle.ts +174 -0
- package/dist/runtime/src/platform/logger.ts +31 -0
- package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
- package/dist/runtime/src/platform/mcp-config.ts +865 -0
- package/dist/runtime/src/platform/modules.ts +145 -0
- package/dist/runtime/src/platform/node-plugins.ts +46 -0
- package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
- package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
- package/dist/runtime/src/prompts/builders.ts +357 -0
- package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
- package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
- package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
- package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
- package/dist/runtime/src/query/active-rooms.ts +532 -0
- package/dist/runtime/src/query/control.ts +313 -0
- package/dist/runtime/src/query/index.ts +24 -0
- package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
- package/dist/runtime/src/query/session-inbox-path.ts +42 -0
- package/dist/runtime/src/query/state.ts +94 -0
- package/dist/runtime/src/query/types.ts +83 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
- package/dist/runtime/src/runtime/attachments.ts +151 -0
- package/dist/runtime/src/runtime/background-sessions.ts +83 -0
- package/dist/runtime/src/runtime/channel-context.ts +107 -0
- package/dist/runtime/src/runtime/errors.ts +69 -0
- package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
- package/dist/runtime/src/runtime/file-hooks.ts +81 -0
- package/dist/runtime/src/runtime/footer.ts +23 -0
- package/dist/runtime/src/runtime/inbox.ts +946 -0
- package/dist/runtime/src/runtime/tasks.ts +56 -0
- package/dist/runtime/src/runtime/topic-config.ts +29 -0
- package/dist/runtime/src/runtime/turn-runner.ts +2286 -0
- package/dist/runtime/src/runtime/usage-alert.ts +84 -0
- package/dist/runtime/src/runtime/visual-html.ts +109 -0
- package/dist/runtime/src/runtime/visual-store.ts +430 -0
- package/dist/runtime/src/runtime/visuals.ts +171 -0
- package/dist/runtime/src/security/sanitize.ts +27 -0
- package/dist/runtime/src/security/sensitive-path.ts +34 -0
- package/dist/runtime/src/storage/activity-log.ts +152 -0
- package/dist/runtime/src/storage/api-messages.ts +617 -0
- package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
- package/dist/runtime/src/storage/api-topic-config.ts +192 -0
- package/dist/runtime/src/storage/api-topics.ts +844 -0
- package/dist/runtime/src/storage/app-settings.ts +44 -0
- package/dist/runtime/src/storage/browser-profiles.ts +133 -0
- package/dist/runtime/src/storage/conversations.ts +268 -0
- package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
- package/dist/runtime/src/storage/forum/index.ts +4 -0
- package/dist/runtime/src/storage/forum/repository.ts +283 -0
- package/dist/runtime/src/storage/forum/schema.ts +300 -0
- package/dist/runtime/src/storage/forum-db.ts +23 -0
- package/dist/runtime/src/storage/runtime-events.ts +130 -0
- package/dist/runtime/src/storage/runtime-leases.ts +189 -0
- package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
- package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +300 -0
- package/dist/runtime/src/storage/self-schedules.ts +298 -0
- package/dist/runtime/src/storage/session-asks.ts +347 -0
- package/dist/runtime/src/storage/sqlite.ts +129 -0
- package/dist/runtime/src/storage/tasks.ts +179 -0
- package/dist/runtime/src/storage/token-stats.ts +144 -0
- package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
- package/dist/runtime/src/storage/topic-archive.ts +74 -0
- package/dist/runtime/src/storage/topic-settings.ts +67 -0
- package/dist/runtime/src/storage/topic-transcript.ts +98 -0
- package/dist/runtime/src/storage/vault-crypto.ts +65 -0
- package/dist/runtime/src/storage/vault.ts +225 -0
- package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
- package/dist/runtime/src/storage/wiki.ts +85 -0
- package/dist/runtime/src/topics/create.ts +123 -0
- package/dist/runtime/src/topics/derive.ts +398 -0
- package/dist/runtime/src/topics/lifecycle.ts +311 -0
- package/dist/runtime/src/topics/links.ts +7 -0
- package/dist/runtime/src/topics/personal-general.ts +65 -0
- package/dist/runtime/src/topics/session.ts +381 -0
- package/dist/runtime/src/types/api.ts +497 -0
- package/dist/runtime/src/types.ts +255 -0
- package/dist/runtime/src/version.ts +1 -0
- package/dist/runtime/tsconfig.json +13 -0
- package/package.json +16 -3
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex rollout encoder.
|
|
3
|
+
*
|
|
4
|
+
* Materializes a synthetic JSONL at
|
|
5
|
+
* `~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-<ts>-<threadId>.jsonl` so a
|
|
6
|
+
* subsequent `codex.resumeThread(threadId)` continues the cross-agent
|
|
7
|
+
* conversation as if it had always been native. Verified by sandbox PoC
|
|
8
|
+
* test-7c: the SDK accepts these without checksum gating, requiring only the
|
|
9
|
+
* captured 5-entry SDK shell (`agents/fixtures/codex-shell.jsonl`) plus
|
|
10
|
+
* synthesized `response_item`/`event_msg` pairs per turn.
|
|
11
|
+
*
|
|
12
|
+
* Tool history is folded into assistant text as `[Tool: ...]` annotations
|
|
13
|
+
* (see shared.ts:extractChatPairs) — the SDK has no fork API and synthetic
|
|
14
|
+
* tool_use IDs across SDKs are too fragile to reconstruct structurally.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
18
|
+
import { existsSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import {
|
|
22
|
+
assertUuidLike,
|
|
23
|
+
type ChatPair,
|
|
24
|
+
clone,
|
|
25
|
+
ensureCwdExists,
|
|
26
|
+
extractChatPairs,
|
|
27
|
+
FIXTURES_DIR,
|
|
28
|
+
} from "#agents/rollout/shared";
|
|
29
|
+
import { parseJsonlText, writeJsonlFile } from "#platform/jsonl";
|
|
30
|
+
import { logger } from "#platform/logger";
|
|
31
|
+
import type { ConversationEntry } from "#storage/conversations";
|
|
32
|
+
import type { EffortLevel } from "#types";
|
|
33
|
+
|
|
34
|
+
interface CodexShell {
|
|
35
|
+
sessionMeta: Record<string, unknown>;
|
|
36
|
+
taskStarted: Record<string, unknown>;
|
|
37
|
+
developerSetup: Record<string, unknown>;
|
|
38
|
+
envContext: Record<string, unknown>;
|
|
39
|
+
turnContext: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function codexSessionsDir(): string {
|
|
43
|
+
return join(process.env.CODEX_HOME || join(homedir(), ".codex"), "sessions");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let _shellCache: CodexShell | null = null;
|
|
47
|
+
function loadCodexShell(): CodexShell {
|
|
48
|
+
if (_shellCache) return _shellCache;
|
|
49
|
+
const raw = readFileSync(join(FIXTURES_DIR, "codex-shell.jsonl"), "utf8");
|
|
50
|
+
const lines = parseJsonlText<Record<string, unknown>>(raw);
|
|
51
|
+
if (lines.length < 5) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`loadCodexShell: expected >=5 entries in codex-shell.jsonl, got ${lines.length}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
_shellCache = {
|
|
57
|
+
sessionMeta: lines[0],
|
|
58
|
+
taskStarted: lines[1],
|
|
59
|
+
developerSetup: lines[2],
|
|
60
|
+
envContext: lines[3],
|
|
61
|
+
turnContext: lines[4],
|
|
62
|
+
};
|
|
63
|
+
return _shellCache;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** UUIDv7 (Codex thread_id format). */
|
|
67
|
+
function uuidv7(): string {
|
|
68
|
+
const ts = Date.now();
|
|
69
|
+
const tsHex = ts.toString(16).padStart(12, "0");
|
|
70
|
+
const rand = randomBytes(10);
|
|
71
|
+
rand[0] = (rand[0] & 0x0f) | 0x70; // version 7
|
|
72
|
+
rand[2] = (rand[2] & 0x3f) | 0x80; // variant
|
|
73
|
+
return [
|
|
74
|
+
tsHex.slice(0, 8),
|
|
75
|
+
tsHex.slice(8, 12),
|
|
76
|
+
rand.slice(0, 2).toString("hex"),
|
|
77
|
+
rand.slice(2, 4).toString("hex"),
|
|
78
|
+
rand.slice(4, 10).toString("hex"),
|
|
79
|
+
].join("-");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Replace captured developer metadata with the policy used for resumed turns. */
|
|
83
|
+
function patchDeveloperSetup(developerSetup: Record<string, unknown>): void {
|
|
84
|
+
const payload = developerSetup.payload as Record<string, unknown> | undefined;
|
|
85
|
+
if (!payload) return;
|
|
86
|
+
payload.content = [
|
|
87
|
+
{
|
|
88
|
+
type: "input_text",
|
|
89
|
+
text: [
|
|
90
|
+
"<permissions instructions>",
|
|
91
|
+
"Filesystem sandboxing is disabled for this runtime turn (`danger-full-access`).",
|
|
92
|
+
"Approval policy is `never`.",
|
|
93
|
+
"</permissions instructions>",
|
|
94
|
+
].join("\n"),
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Mutate captured `<environment_context>` fields to the current runtime. */
|
|
100
|
+
function patchEnvContext(
|
|
101
|
+
envContext: Record<string, unknown>,
|
|
102
|
+
cwd: string,
|
|
103
|
+
currentDate: string,
|
|
104
|
+
timezone: string,
|
|
105
|
+
): void {
|
|
106
|
+
const payload = envContext.payload as Record<string, unknown> | undefined;
|
|
107
|
+
if (!payload) return;
|
|
108
|
+
const content = payload.content as Array<Record<string, unknown>> | undefined;
|
|
109
|
+
if (!Array.isArray(content)) return;
|
|
110
|
+
for (const block of content) {
|
|
111
|
+
if (block.type === "input_text" && typeof block.text === "string") {
|
|
112
|
+
block.text = block.text
|
|
113
|
+
.replace(/<cwd>[^<]*<\/cwd>/, `<cwd>${cwd}</cwd>`)
|
|
114
|
+
.replace(
|
|
115
|
+
/<current_date>[^<]*<\/current_date>/,
|
|
116
|
+
`<current_date>${currentDate}</current_date>`,
|
|
117
|
+
)
|
|
118
|
+
.replace(/<timezone>[^<]*<\/timezone>/, `<timezone>${timezone}</timezone>`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface CodexRolloutOptions {
|
|
124
|
+
/** Working directory the resumed Codex thread will report. */
|
|
125
|
+
cwd: string;
|
|
126
|
+
/** Optional override; default = freshly generated UUIDv7. */
|
|
127
|
+
threadId?: string;
|
|
128
|
+
/** Override extracted pairs. */
|
|
129
|
+
pairs?: ChatPair[];
|
|
130
|
+
/** Source UnifiedEvent log when `pairs` is omitted. */
|
|
131
|
+
entries?: ConversationEntry[];
|
|
132
|
+
/** Effective model/effort that Codex will use when this thread is resumed. */
|
|
133
|
+
model?: string;
|
|
134
|
+
effort?: EffortLevel;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface CodexRolloutResult {
|
|
138
|
+
threadId: string;
|
|
139
|
+
rolloutPath: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface CodexContextUsage {
|
|
143
|
+
contextTokens: number;
|
|
144
|
+
contextWindow: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Read the latest per-request context measurement from a Codex rollout. */
|
|
148
|
+
export function extractLatestCodexContextUsage(jsonl: string): CodexContextUsage | undefined {
|
|
149
|
+
const lines = jsonl.trimEnd().split("\n");
|
|
150
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
151
|
+
try {
|
|
152
|
+
const entry = JSON.parse(lines[index] ?? "") as {
|
|
153
|
+
type?: string;
|
|
154
|
+
payload?: {
|
|
155
|
+
type?: string;
|
|
156
|
+
info?: {
|
|
157
|
+
last_token_usage?: { total_tokens?: number };
|
|
158
|
+
model_context_window?: number;
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
if (entry.type !== "event_msg" || entry.payload?.type !== "token_count") continue;
|
|
163
|
+
const contextTokens = entry.payload.info?.last_token_usage?.total_tokens;
|
|
164
|
+
const contextWindow = entry.payload.info?.model_context_window;
|
|
165
|
+
if (
|
|
166
|
+
typeof contextTokens === "number" &&
|
|
167
|
+
Number.isFinite(contextTokens) &&
|
|
168
|
+
contextTokens >= 0 &&
|
|
169
|
+
typeof contextWindow === "number" &&
|
|
170
|
+
Number.isFinite(contextWindow) &&
|
|
171
|
+
contextWindow > 0
|
|
172
|
+
) {
|
|
173
|
+
return { contextTokens, contextWindow };
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// A concurrently appended final line may be incomplete; keep scanning.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Resolve a thread's current rollout and return its latest context measurement. */
|
|
183
|
+
export function readLatestCodexContextUsage(threadId: string): CodexContextUsage | undefined {
|
|
184
|
+
const sessionsDir = codexSessionsDir();
|
|
185
|
+
const candidates: string[] = [];
|
|
186
|
+
const buckets = candidateDateBuckets(threadId);
|
|
187
|
+
try {
|
|
188
|
+
if (buckets) {
|
|
189
|
+
for (const bucket of buckets) {
|
|
190
|
+
const dir = join(sessionsDir, bucket);
|
|
191
|
+
if (!existsSync(dir)) continue;
|
|
192
|
+
const glob = new Bun.Glob(`rollout-*-${threadId}.jsonl`);
|
|
193
|
+
for (const rel of glob.scanSync({ cwd: dir, onlyFiles: true })) {
|
|
194
|
+
candidates.push(join(dir, rel));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
const glob = new Bun.Glob(`**/rollout-*-${threadId}.jsonl`);
|
|
199
|
+
for (const rel of glob.scanSync({ cwd: sessionsDir, onlyFiles: true })) {
|
|
200
|
+
candidates.push(join(sessionsDir, rel));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const path = candidates.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
|
|
204
|
+
return path ? extractLatestCodexContextUsage(readFileSync(path, "utf8")) : undefined;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
logger.debug({ error, threadId }, "codex context usage: rollout read failed");
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Old and metadata-free Codex rollouts resume as native multi-agent v1 even
|
|
213
|
+
* when the feature flag is false. Rewrite their runtime metadata in place so
|
|
214
|
+
* the same thread can resume without losing provider conversation history.
|
|
215
|
+
*/
|
|
216
|
+
export function migrateCodexRolloutNativeMultiAgentMetadata(threadId: string): boolean {
|
|
217
|
+
const path = latestCodexRolloutPath(threadId);
|
|
218
|
+
if (!path) return false;
|
|
219
|
+
try {
|
|
220
|
+
const entries = parseJsonlText<{
|
|
221
|
+
type?: string;
|
|
222
|
+
payload?: Record<string, unknown>;
|
|
223
|
+
}>(readFileSync(path, "utf8"));
|
|
224
|
+
let changed = false;
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
if (entry.type !== "session_meta" && entry.type !== "turn_context") continue;
|
|
227
|
+
if (!entry.payload) entry.payload = {};
|
|
228
|
+
if (entry.payload.multi_agent_version !== "disabled") {
|
|
229
|
+
entry.payload.multi_agent_version = "disabled";
|
|
230
|
+
changed = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (changed) {
|
|
234
|
+
writeJsonlFile(path, entries);
|
|
235
|
+
logger.info({ threadId, path }, "codex rollout native multi-agent metadata disabled");
|
|
236
|
+
}
|
|
237
|
+
return changed;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
logger.error({ error, threadId, path }, "codex rollout native multi-agent migration failed");
|
|
240
|
+
throw new Error(`Failed to migrate Codex rollout '${threadId}'`, { cause: error });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function latestCodexRolloutPath(threadId: string): string | undefined {
|
|
245
|
+
const sessionsDir = codexSessionsDir();
|
|
246
|
+
const candidates: string[] = [];
|
|
247
|
+
const buckets = candidateDateBuckets(threadId);
|
|
248
|
+
try {
|
|
249
|
+
if (buckets) {
|
|
250
|
+
for (const bucket of buckets) {
|
|
251
|
+
const dir = join(sessionsDir, bucket);
|
|
252
|
+
if (!existsSync(dir)) continue;
|
|
253
|
+
const glob = new Bun.Glob(`rollout-*-${threadId}.jsonl`);
|
|
254
|
+
for (const rel of glob.scanSync({ cwd: dir, onlyFiles: true })) {
|
|
255
|
+
candidates.push(join(dir, rel));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// The UUIDv7 timestamp is UTC, but Codex names session directories by the
|
|
260
|
+
// local date, so a session opened in the evening of a negative-offset
|
|
261
|
+
// timezone lands in the previous day's bucket. When the date-derived
|
|
262
|
+
// buckets miss (or none were computed), fall back to a whole-tree scan so
|
|
263
|
+
// that skew never silently drops the rollout.
|
|
264
|
+
if (candidates.length === 0) {
|
|
265
|
+
const glob = new Bun.Glob(`**/rollout-*-${threadId}.jsonl`);
|
|
266
|
+
for (const rel of glob.scanSync({ cwd: sessionsDir, onlyFiles: true })) {
|
|
267
|
+
candidates.push(join(sessionsDir, rel));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return candidates.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
|
|
271
|
+
} catch (error) {
|
|
272
|
+
logger.debug({ error, threadId }, "codex rollout lookup failed");
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Materialize a Codex rollout JSONL with the provided dialogue and place it
|
|
279
|
+
* at `~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-<ts>-<threadId>.jsonl` so a
|
|
280
|
+
* subsequent `codex.resumeThread(threadId)` continues the conversation.
|
|
281
|
+
*/
|
|
282
|
+
export function writeCodexRollout(opts: CodexRolloutOptions): CodexRolloutResult {
|
|
283
|
+
const threadId = opts.threadId ?? uuidv7();
|
|
284
|
+
assertUuidLike("threadId", threadId);
|
|
285
|
+
ensureCwdExists(opts.cwd);
|
|
286
|
+
const pairs = opts.pairs ?? extractChatPairs(opts.entries ?? []);
|
|
287
|
+
const now = new Date();
|
|
288
|
+
const tsIso = now.toISOString();
|
|
289
|
+
const currentDate = [
|
|
290
|
+
now.getFullYear(),
|
|
291
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
292
|
+
String(now.getDate()).padStart(2, "0"),
|
|
293
|
+
].join("-");
|
|
294
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || process.env.TZ || "UTC";
|
|
295
|
+
|
|
296
|
+
// Captured 5-entry SDK shell (session_meta, task_started, developer setup,
|
|
297
|
+
// environment_context, turn_context). Cloned per call so the cached
|
|
298
|
+
// fixture is never mutated; only id/timestamp/cwd are patched.
|
|
299
|
+
const shell = loadCodexShell();
|
|
300
|
+
const sessionMeta = clone(shell.sessionMeta);
|
|
301
|
+
(sessionMeta.payload as Record<string, unknown>).id = threadId;
|
|
302
|
+
(sessionMeta.payload as Record<string, unknown>).timestamp = tsIso;
|
|
303
|
+
(sessionMeta.payload as Record<string, unknown>).cwd = opts.cwd;
|
|
304
|
+
(sessionMeta.payload as Record<string, unknown>).multi_agent_version = "disabled";
|
|
305
|
+
(sessionMeta as Record<string, unknown>).timestamp = tsIso;
|
|
306
|
+
|
|
307
|
+
const taskStarted = clone(shell.taskStarted);
|
|
308
|
+
(taskStarted as Record<string, unknown>).timestamp = tsIso;
|
|
309
|
+
|
|
310
|
+
const developerSetup = clone(shell.developerSetup);
|
|
311
|
+
(developerSetup as Record<string, unknown>).timestamp = tsIso;
|
|
312
|
+
patchDeveloperSetup(developerSetup);
|
|
313
|
+
|
|
314
|
+
const envContext = clone(shell.envContext);
|
|
315
|
+
(envContext as Record<string, unknown>).timestamp = tsIso;
|
|
316
|
+
patchEnvContext(envContext, opts.cwd, currentDate, timezone);
|
|
317
|
+
|
|
318
|
+
const turnContext = clone(shell.turnContext);
|
|
319
|
+
(turnContext as Record<string, unknown>).timestamp = tsIso;
|
|
320
|
+
const turnPayload = turnContext.payload as Record<string, unknown>;
|
|
321
|
+
turnPayload.cwd = opts.cwd;
|
|
322
|
+
turnPayload.current_date = currentDate;
|
|
323
|
+
turnPayload.timezone = timezone;
|
|
324
|
+
turnPayload.approval_policy = "never";
|
|
325
|
+
turnPayload.sandbox_policy = { type: "danger-full-access" };
|
|
326
|
+
turnPayload.multi_agent_version = "disabled";
|
|
327
|
+
delete turnPayload.permission_profile;
|
|
328
|
+
if (opts.model) turnPayload.model = opts.model;
|
|
329
|
+
const collaborationMode = turnPayload.collaboration_mode as Record<string, unknown> | undefined;
|
|
330
|
+
const collaborationSettings = collaborationMode?.settings as Record<string, unknown> | undefined;
|
|
331
|
+
if (collaborationSettings) {
|
|
332
|
+
if (opts.model) collaborationSettings.model = opts.model;
|
|
333
|
+
if (opts.effort !== undefined) collaborationSettings.reasoning_effort = opts.effort;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const lines: unknown[] = [sessionMeta, taskStarted, developerSetup, envContext, turnContext];
|
|
337
|
+
|
|
338
|
+
for (const pair of pairs) {
|
|
339
|
+
lines.push({
|
|
340
|
+
timestamp: tsIso,
|
|
341
|
+
type: "response_item",
|
|
342
|
+
payload: {
|
|
343
|
+
type: "message",
|
|
344
|
+
role: "user",
|
|
345
|
+
content: [{ type: "input_text", text: pair.userText }],
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
lines.push({
|
|
349
|
+
timestamp: tsIso,
|
|
350
|
+
type: "event_msg",
|
|
351
|
+
payload: {
|
|
352
|
+
type: "user_message",
|
|
353
|
+
message: pair.userText,
|
|
354
|
+
images: [],
|
|
355
|
+
local_images: [],
|
|
356
|
+
text_elements: [],
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
lines.push({
|
|
360
|
+
timestamp: tsIso,
|
|
361
|
+
type: "event_msg",
|
|
362
|
+
payload: {
|
|
363
|
+
type: "agent_message",
|
|
364
|
+
message: pair.assistantText,
|
|
365
|
+
phase: "final_answer",
|
|
366
|
+
memory_citation: null,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
lines.push({
|
|
370
|
+
timestamp: tsIso,
|
|
371
|
+
type: "response_item",
|
|
372
|
+
payload: {
|
|
373
|
+
type: "message",
|
|
374
|
+
role: "assistant",
|
|
375
|
+
content: [{ type: "output_text", text: pair.assistantText }],
|
|
376
|
+
phase: "final_answer",
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
lines.push({
|
|
382
|
+
timestamp: tsIso,
|
|
383
|
+
type: "event_msg",
|
|
384
|
+
payload: { type: "task_complete" },
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const yyyy = now.getUTCFullYear();
|
|
388
|
+
const mm = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
389
|
+
const dd = String(now.getUTCDate()).padStart(2, "0");
|
|
390
|
+
const tsStr = tsIso.replace(/[:.]/g, "-").slice(0, 19);
|
|
391
|
+
const dir = join(codexSessionsDir(), String(yyyy), mm, dd);
|
|
392
|
+
const path = join(dir, `rollout-${tsStr}-${threadId}.jsonl`);
|
|
393
|
+
|
|
394
|
+
// Round-trip cleanup: when the caller reused a threadId (claude→codex→...
|
|
395
|
+
// or maestro→codex→... cycles via `findLastSessionIdForAgent`), the SDK's
|
|
396
|
+
// original rollout for that threadId still sits at its ORIGINAL-date
|
|
397
|
+
// directory (`~/.codex/sessions/<orig-yyyy>/<orig-mm>/<orig-dd>/...`).
|
|
398
|
+
// Writing today's synthetic rollout next to it leaves two jsonls sharing
|
|
399
|
+
// the same threadId — `codex resume <tid>` globs `~/.codex/sessions/**`
|
|
400
|
+
// and the SDK has no spec for which match it picks, so behavior becomes
|
|
401
|
+
// environment-/codex-version-dependent. Sweep every prior file for this
|
|
402
|
+
// threadId BEFORE writing the new one so resume always reads our synth.
|
|
403
|
+
//
|
|
404
|
+
// Synchronous on purpose: writeCodexRollout's signature is sync (called
|
|
405
|
+
// inside set_agent's DB transaction), and an async sweep would race with
|
|
406
|
+
// the following `writeJsonlFile` — Bun.Glob would re-scan once the
|
|
407
|
+
// microtask queue drains and could unlink the file we just wrote if today
|
|
408
|
+
// and the original-write-date are the same. Sync glob/unlink completes
|
|
409
|
+
// before the new file ever lands on disk.
|
|
410
|
+
//
|
|
411
|
+
// Only runs when `opts.threadId` was supplied; fresh-mint threadIds can't
|
|
412
|
+
// have prior files. Errors are logged but non-fatal — losing the sweep on
|
|
413
|
+
// a permission glitch is preferable to crashing the whole switch.
|
|
414
|
+
if (opts.threadId) {
|
|
415
|
+
sweepPriorRolloutsForThread(opts.threadId);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
writeJsonlFile(path, lines);
|
|
419
|
+
logger.info(
|
|
420
|
+
{ threadId, path, pairs: pairs.length },
|
|
421
|
+
"writeCodexRollout: synthetic rollout placed",
|
|
422
|
+
);
|
|
423
|
+
return { threadId, rolloutPath: path };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Synchronous unlink of every existing `rollout-*-<threadId>.jsonl` under
|
|
428
|
+
* `~/.codex/sessions/**`. Same glob shape `codexRegistry.cleanupRollouts`
|
|
429
|
+
* uses (kept duplicate rather than imported to avoid the registry → rollout
|
|
430
|
+
* → registry cycle).
|
|
431
|
+
*/
|
|
432
|
+
function sweepPriorRolloutsForThread(threadId: string): void {
|
|
433
|
+
const sessionsDir = codexSessionsDir();
|
|
434
|
+
const buckets = candidateDateBuckets(threadId);
|
|
435
|
+
if (!buckets) {
|
|
436
|
+
// Couldn't pin a date from the threadId — fall back to the safer (but
|
|
437
|
+
// much slower) whole-tree scan so we still find every prior rollout.
|
|
438
|
+
sweepPriorRolloutsFullTree(threadId, sessionsDir);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
for (const bucket of buckets) {
|
|
442
|
+
const dir = join(sessionsDir, bucket);
|
|
443
|
+
if (!existsSync(dir)) continue;
|
|
444
|
+
try {
|
|
445
|
+
const glob = new Bun.Glob(`rollout-*-${threadId}.jsonl`);
|
|
446
|
+
for (const rel of glob.scanSync({ cwd: dir, onlyFiles: true })) {
|
|
447
|
+
const fullPath = join(dir, rel);
|
|
448
|
+
try {
|
|
449
|
+
unlinkSync(fullPath);
|
|
450
|
+
} catch (e) {
|
|
451
|
+
if ((e as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
452
|
+
logger.warn(
|
|
453
|
+
{ err: e, path: fullPath },
|
|
454
|
+
"writeCodexRollout: prior-rollout unlink failed (continuing)",
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} catch (e) {
|
|
460
|
+
logger.warn(
|
|
461
|
+
{ err: e, threadId, bucket },
|
|
462
|
+
"writeCodexRollout: prior-rollout bucket scan failed (continuing)",
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Legacy whole-tree scan. Used only when `candidateDateBuckets` can't pin a
|
|
469
|
+
* date set from the threadId (non-UUIDv7 ids, e.g. some test fixtures). */
|
|
470
|
+
function sweepPriorRolloutsFullTree(threadId: string, sessionsDir: string): void {
|
|
471
|
+
try {
|
|
472
|
+
const glob = new Bun.Glob(`**/rollout-*-${threadId}.jsonl`);
|
|
473
|
+
for (const rel of glob.scanSync({ cwd: sessionsDir, onlyFiles: true })) {
|
|
474
|
+
const fullPath = join(sessionsDir, rel);
|
|
475
|
+
try {
|
|
476
|
+
unlinkSync(fullPath);
|
|
477
|
+
} catch (e) {
|
|
478
|
+
if ((e as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
479
|
+
logger.warn(
|
|
480
|
+
{ err: e, path: fullPath },
|
|
481
|
+
"writeCodexRollout: prior-rollout unlink failed (continuing)",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
} catch (e) {
|
|
487
|
+
logger.warn(
|
|
488
|
+
{ err: e, threadId },
|
|
489
|
+
"writeCodexRollout: full-tree prior-rollout sweep failed (continuing)",
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* UUIDv7 ids encode their creation time in the first 48 bits. `writeCodexRollout`
|
|
496
|
+
* buckets new files at `<yyyy>/<mm>/<dd>/` keyed off the WRITE time, so a
|
|
497
|
+
* thread's rollouts can only live between (threadId's birth date) and
|
|
498
|
+
* (today's date) — typically a 0-2 day span. We enumerate every day in that
|
|
499
|
+
* inclusive range so a Mon-bridge / Tue-rebridge picks up both.
|
|
500
|
+
*
|
|
501
|
+
* Returns null when:
|
|
502
|
+
* - threadId isn't a parseable UUIDv7 (atypical fixture / future format)
|
|
503
|
+
* - the day span exceeds MAX_DAY_SPAN (a month+-old re-bridge isn't worth
|
|
504
|
+
* per-day enumeration; safer to scan the full tree than to enumerate
|
|
505
|
+
* hundreds of dirs and still maybe miss something)
|
|
506
|
+
* In both cases the caller falls back to the whole-tree sweep so safety
|
|
507
|
+
* never degrades below the original implementation.
|
|
508
|
+
*/
|
|
509
|
+
const MAX_DAY_SPAN = 30;
|
|
510
|
+
const DAY_MS = 86_400_000;
|
|
511
|
+
const LOCAL_DATE_SKEW_DAYS = 1;
|
|
512
|
+
|
|
513
|
+
function candidateDateBuckets(threadId: string): Set<string> | null {
|
|
514
|
+
const tidMs = decodeUuidV7Timestamp(threadId);
|
|
515
|
+
if (tidMs === null) return null;
|
|
516
|
+
const nowMs = Date.now();
|
|
517
|
+
// Day-floor each end so DST / minutes-of-day noise doesn't bloat the span.
|
|
518
|
+
const tidDay = Math.floor(tidMs / DAY_MS);
|
|
519
|
+
const todayDay = Math.floor(nowMs / DAY_MS);
|
|
520
|
+
const minDay = Math.min(tidDay, todayDay);
|
|
521
|
+
const maxDay = Math.max(tidDay, todayDay);
|
|
522
|
+
const span = maxDay - minDay;
|
|
523
|
+
if (span > MAX_DAY_SPAN) return null;
|
|
524
|
+
const buckets = new Set<string>();
|
|
525
|
+
// Codex buckets sessions by local calendar date while UUIDv7 timestamps are
|
|
526
|
+
// UTC. Include one day on each edge so every date-bucketed caller (context
|
|
527
|
+
// usage, migration, and prior-rollout cleanup) covers timezone skew without
|
|
528
|
+
// falling back to a whole-tree scan.
|
|
529
|
+
for (let d = minDay - LOCAL_DATE_SKEW_DAYS; d <= maxDay + LOCAL_DATE_SKEW_DAYS; d++) {
|
|
530
|
+
buckets.add(formatDateBucket(new Date(d * DAY_MS)));
|
|
531
|
+
}
|
|
532
|
+
return buckets;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Decode the 48-bit unix-ms timestamp out of a UUIDv7. The first 12 hex
|
|
537
|
+
* chars of the dash-separated form are the timestamp. Returns null when the
|
|
538
|
+
* input doesn't look like a UUIDv7 or the decoded value is outside a
|
|
539
|
+
* plausibility window (defensive: a malformed id that fits the regex but
|
|
540
|
+
* decodes to year 1972 would point the sweep at a non-existent bucket —
|
|
541
|
+
* better to fall back to the whole-tree scan than to silently no-op).
|
|
542
|
+
*
|
|
543
|
+
* Exported for tests.
|
|
544
|
+
*/
|
|
545
|
+
export function decodeUuidV7Timestamp(threadId: string): number | null {
|
|
546
|
+
if (typeof threadId !== "string") return null;
|
|
547
|
+
const m =
|
|
548
|
+
/^([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-7[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.exec(
|
|
549
|
+
threadId,
|
|
550
|
+
);
|
|
551
|
+
if (!m) return null;
|
|
552
|
+
const hex = `${m[1]}${m[2]}`;
|
|
553
|
+
const ms = Number.parseInt(hex, 16);
|
|
554
|
+
if (!Number.isFinite(ms)) return null;
|
|
555
|
+
const MIN = 1577836800000; // 2020-01-01T00:00:00Z
|
|
556
|
+
const MAX = 4102444800000; // 2100-01-01T00:00:00Z
|
|
557
|
+
if (ms < MIN || ms > MAX) return null;
|
|
558
|
+
return ms;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function formatDateBucket(d: Date): string {
|
|
562
|
+
const yyyy = d.getUTCFullYear();
|
|
563
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
564
|
+
const dd = String(d.getUTCDate()).padStart(2, "0");
|
|
565
|
+
return `${yyyy}/${mm}/${dd}`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Internal export for tests. */
|
|
569
|
+
export const __candidateDateBuckets = candidateDateBuckets;
|