mixdog 0.9.150 → 0.9.152
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 +110 -51
- package/package.json +3 -2
- package/scripts/tool-stress.mjs +0 -2
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +19 -13
- package/src/headless-exec.test.mjs +46 -8
- package/src/rules/shared/30-exploration.md +2 -1
- package/src/runtime/agent/orchestrator/config.mjs +6 -66
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/handoff.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +42 -34
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +23 -44
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +22 -125
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +9 -23
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/absolute-glob-expand.test.mjs +63 -0
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +29 -10
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/enoent-outside-project.test.mjs +56 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +105 -31
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +103 -1
- package/src/runtime/agent/orchestrator/tools/builtin/grep-single-file-rescue.test.mjs +106 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/absolute-glob-expand.mjs +129 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +218 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +26 -652
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/noise-dir-visibility.test.mjs +69 -0
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +33 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +26 -6
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +7 -66
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +60 -5
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +54 -1
- package/src/runtime/agent/orchestrator/tools/builtin/write-symlink.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-anchor-relocation.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +78 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +44 -10
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +57 -5
- package/src/runtime/agent/orchestrator/tools/patch/patch-symlink.test.mjs +141 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/patch/v4a-pure-move.test.mjs +57 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-section-coalesce.test.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/browser-bridge/client.mjs +103 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
- package/src/runtime/computer-bridge/client.mjs +101 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +3 -5
- package/src/runtime/memory/lib/pg/process.mjs +29 -2
- package/src/runtime/memory/lib/pg/process.test.mjs +36 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +5 -1
- package/src/runtime/memory/lib/query-handlers.mjs +7 -6
- package/src/runtime/memory/lib/recall-format.mjs +13 -31
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +5 -1
- package/src/runtime/shared/pristine-execution.mjs +5 -2
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-surface.mjs +18 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/goal-runtime.mjs +721 -0
- package/src/session-runtime/goal-runtime.test.mjs +108 -0
- package/src/session-runtime/lifecycle-api.mjs +19 -4
- package/src/session-runtime/lifecycle-api.test.mjs +21 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/resource-api-global.test.mjs +69 -0
- package/src/session-runtime/resource-api.mjs +173 -58
- package/src/session-runtime/runtime-core.mjs +148 -20
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-turn-api.mjs +22 -11
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +4 -13
- package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
- package/src/session-runtime/tool-surface.mjs +12 -0
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/notify.mjs +6 -8
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/channel-worker-heartbeat.test.mjs +30 -0
- package/src/standalone/channel-worker.mjs +31 -1
- package/src/standalone/daemon.mjs +31 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/memory-runtime-proxy.mjs +5 -2
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-client.mjs +23 -3
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +696 -58
- package/src/standalone/session-runtime-inline-host.mjs +351 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +476 -18
- package/src/standalone/session-service.mjs +81 -6
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +163 -44
- package/src/tui/session/context-state.mjs +1 -0
- package/src/tui/session/goal-continuation.mjs +110 -0
- package/src/tui/session/goal-continuation.test.mjs +84 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +108 -0
- package/src/tui/session/session-api-ext.mjs +39 -8
- package/src/tui/session/session-api.mjs +58 -0
- package/src/tui/session/session-flow.mjs +6 -0
- package/src/tui/session/turn.mjs +13 -0
- package/src/tui/session-local.mjs +15 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Deterministic routing for the session runtime SHARD host.
|
|
4
|
+
//
|
|
5
|
+
// One child process owning every session runtime, every agent dispatch, every
|
|
6
|
+
// provider parse and every tool result is a single event-loop failure domain:
|
|
7
|
+
// one slow parse or one runaway tool stalls unrelated sessions and their
|
|
8
|
+
// control/abort path. The host therefore runs a BOUNDED set of identical
|
|
9
|
+
// runtime children ("shards") and routes work to them deterministically.
|
|
10
|
+
//
|
|
11
|
+
// Routing rules (all pure, all testable here):
|
|
12
|
+
// * Ownership is keyed by session identity (sessionId), not by arrival
|
|
13
|
+
// order, so every view/recovery/resume of one session lands on the SAME
|
|
14
|
+
// shard and its in-process session store stays authoritative.
|
|
15
|
+
// * A key's home shard is a stable hash; probing continues in a stable
|
|
16
|
+
// rotation only while the home shard is not placeable (lagging/quarantined).
|
|
17
|
+
// * Once a key is claimed it keeps its shard until the last holder releases
|
|
18
|
+
// it — health changes never migrate live work, which would strand accepted
|
|
19
|
+
// input in the abandoned child.
|
|
20
|
+
|
|
21
|
+
export const SESSION_RUNTIME_SHARD_ENV = 'MIXDOG_SESSION_RUNTIME_SHARDS';
|
|
22
|
+
export const MAX_SESSION_RUNTIME_SHARDS = 16;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The external-host compatibility seam defaults to one shared runtime process.
|
|
26
|
+
* Production sessions now use the daemon's in-process actor host instead.
|
|
27
|
+
*
|
|
28
|
+
* The legacy env is intentionally ignored. Explicit multi-process counts stay
|
|
29
|
+
* available only through createSessionRuntimeHost({ shardCount }) as a test
|
|
30
|
+
* seam for recovery and routing contracts.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveShardCount() {
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** FNV-1a (32-bit): stable across processes and daemon restarts. */
|
|
37
|
+
export function hashRoutingKey(key) {
|
|
38
|
+
const text = String(key ?? '');
|
|
39
|
+
let hash = 0x811c9dc5;
|
|
40
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
41
|
+
hash ^= text.charCodeAt(index) & 0xff;
|
|
42
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
43
|
+
}
|
|
44
|
+
return hash >>> 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function normalizeShardCount(shardCount) {
|
|
48
|
+
const count = Math.floor(Number(shardCount) || 1);
|
|
49
|
+
return Math.max(1, Math.min(MAX_SESSION_RUNTIME_SHARDS, count));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Home shard for a routing key. */
|
|
53
|
+
export function shardIndexForKey(key, shardCount) {
|
|
54
|
+
const count = normalizeShardCount(shardCount);
|
|
55
|
+
return hashRoutingKey(key) % count;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Home shard first, then a stable rotation over the remaining shards. */
|
|
59
|
+
export function shardProbeOrder(key, shardCount) {
|
|
60
|
+
const count = normalizeShardCount(shardCount);
|
|
61
|
+
const home = shardIndexForKey(key, count);
|
|
62
|
+
const order = [];
|
|
63
|
+
for (let step = 0; step < count; step += 1) order.push((home + step) % count);
|
|
64
|
+
return order;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Placement for a NEW key. Falls back through the probe order while shards are
|
|
69
|
+
* not placeable; when every shard is unplaceable the home shard still owns the
|
|
70
|
+
* work (queued, never dropped).
|
|
71
|
+
*/
|
|
72
|
+
export function selectShardIndex(key, shardCount, isPlaceable = () => true) {
|
|
73
|
+
const order = shardProbeOrder(key, shardCount);
|
|
74
|
+
for (const index of order) {
|
|
75
|
+
let ok = true;
|
|
76
|
+
try { ok = isPlaceable(index) !== false; } catch { ok = true; }
|
|
77
|
+
if (ok) return index;
|
|
78
|
+
}
|
|
79
|
+
return order[0];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Routing key for a session runtime: session identity when known. */
|
|
83
|
+
export function runtimeRoutingKey(options, fallbackKey = '') {
|
|
84
|
+
const sessionId = String(options?.sessionId || '').trim();
|
|
85
|
+
return sessionId || String(fallbackKey || '');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Refcounted key -> shard ownership. Ownership is sticky for as long as any
|
|
90
|
+
* holder exists so recovery/resume of the same session never changes shard.
|
|
91
|
+
*/
|
|
92
|
+
export function createShardOwnership() {
|
|
93
|
+
const owners = new Map(); // key -> { index, refs }
|
|
94
|
+
return {
|
|
95
|
+
claim(key, resolve) {
|
|
96
|
+
const id = String(key || '');
|
|
97
|
+
const existing = owners.get(id);
|
|
98
|
+
if (existing) {
|
|
99
|
+
existing.refs += 1;
|
|
100
|
+
return existing.index;
|
|
101
|
+
}
|
|
102
|
+
const index = Math.max(0, Math.floor(Number(resolve(id)) || 0));
|
|
103
|
+
owners.set(id, { index, refs: 1 });
|
|
104
|
+
return index;
|
|
105
|
+
},
|
|
106
|
+
peek(key) {
|
|
107
|
+
const existing = owners.get(String(key || ''));
|
|
108
|
+
return existing ? existing.index : null;
|
|
109
|
+
},
|
|
110
|
+
release(key) {
|
|
111
|
+
const id = String(key || '');
|
|
112
|
+
const existing = owners.get(id);
|
|
113
|
+
if (!existing) return null;
|
|
114
|
+
existing.refs -= 1;
|
|
115
|
+
if (existing.refs <= 0) owners.delete(id);
|
|
116
|
+
return existing.index;
|
|
117
|
+
},
|
|
118
|
+
get size() { return owners.size; },
|
|
119
|
+
entries() { return [...owners].map(([key, value]) => ({ key, ...value })); },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Provider capacity cooldown is an ACCOUNT-wide fact, but each shard discovers
|
|
125
|
+
* it in its own process. The host merges the strongest known cooldown and
|
|
126
|
+
* replays it to the other shards; merging must be monotonic so a replay can
|
|
127
|
+
* never shorten or clear a live cooldown (that would re-open the hammering
|
|
128
|
+
* window this state exists to close).
|
|
129
|
+
*/
|
|
130
|
+
export function mergeProviderCooldown(current, incoming) {
|
|
131
|
+
const base = {
|
|
132
|
+
untilMs: Number(current?.untilMs) || 0,
|
|
133
|
+
disabledReason: current?.disabledReason ? String(current.disabledReason) : null,
|
|
134
|
+
updatedAt: Number(current?.updatedAt) || 0,
|
|
135
|
+
};
|
|
136
|
+
const untilMs = Number(incoming?.untilMs) || 0;
|
|
137
|
+
const disabledReason = incoming?.disabledReason ? String(incoming.disabledReason) : null;
|
|
138
|
+
const grew = untilMs > base.untilMs + 1_000;
|
|
139
|
+
const newlyDisabled = Boolean(disabledReason) && disabledReason !== base.disabledReason;
|
|
140
|
+
if (!grew && !newlyDisabled) return { changed: false, cooldown: base };
|
|
141
|
+
return {
|
|
142
|
+
changed: true,
|
|
143
|
+
cooldown: {
|
|
144
|
+
untilMs: Math.max(base.untilMs, untilMs),
|
|
145
|
+
disabledReason: disabledReason || base.disabledReason,
|
|
146
|
+
updatedAt: Number(incoming?.observedAt) || Date.now(),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createShardOwnership,
|
|
6
|
+
hashRoutingKey,
|
|
7
|
+
normalizeShardCount,
|
|
8
|
+
mergeProviderCooldown,
|
|
9
|
+
resolveShardCount,
|
|
10
|
+
runtimeRoutingKey,
|
|
11
|
+
selectShardIndex,
|
|
12
|
+
shardIndexForKey,
|
|
13
|
+
shardProbeOrder,
|
|
14
|
+
} from './session-runtime-shard-router.mjs';
|
|
15
|
+
|
|
16
|
+
test('production always uses one shared actor runtime process', () => {
|
|
17
|
+
assert.equal(resolveShardCount({}, 1), 1);
|
|
18
|
+
assert.equal(resolveShardCount({}, 2), 1);
|
|
19
|
+
assert.equal(resolveShardCount({}, 128), 1);
|
|
20
|
+
assert.equal(resolveShardCount({ MIXDOG_SESSION_RUNTIME_SHARDS: '6' }, 128), 1);
|
|
21
|
+
// Explicit constructor counts remain a bounded recovery/routing test seam.
|
|
22
|
+
assert.equal(normalizeShardCount(999), 16);
|
|
23
|
+
assert.equal(normalizeShardCount(0), 1);
|
|
24
|
+
assert.equal(normalizeShardCount(-3), 1);
|
|
25
|
+
assert.equal(normalizeShardCount(Number.NaN), 1);
|
|
26
|
+
assert.equal(normalizeShardCount(3), 3);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('routing is deterministic, in range, and spreads keys across shards', () => {
|
|
30
|
+
assert.equal(hashRoutingKey('sess-1'), hashRoutingKey('sess-1'));
|
|
31
|
+
const used = new Set();
|
|
32
|
+
for (let index = 0; index < 200; index += 1) {
|
|
33
|
+
const key = `sess-${index}`;
|
|
34
|
+
const shard = shardIndexForKey(key, 4);
|
|
35
|
+
assert.equal(shard, shardIndexForKey(key, 4));
|
|
36
|
+
assert.ok(shard >= 0 && shard < 4);
|
|
37
|
+
used.add(shard);
|
|
38
|
+
}
|
|
39
|
+
// Fan-out is the point: a batch of independent sessions must not collapse
|
|
40
|
+
// onto one event loop.
|
|
41
|
+
assert.equal(used.size, 4);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('probe order starts at the home shard and covers every shard once', () => {
|
|
45
|
+
const order = shardProbeOrder('sess-probe', 4);
|
|
46
|
+
assert.equal(order.length, 4);
|
|
47
|
+
assert.equal(order[0], shardIndexForKey('sess-probe', 4));
|
|
48
|
+
assert.equal(new Set(order).size, 4);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('placement skips unplaceable shards but never drops the work', () => {
|
|
52
|
+
const key = 'sess-degraded';
|
|
53
|
+
const home = shardIndexForKey(key, 3);
|
|
54
|
+
const next = shardProbeOrder(key, 3)[1];
|
|
55
|
+
assert.equal(selectShardIndex(key, 3, () => true), home);
|
|
56
|
+
assert.equal(selectShardIndex(key, 3, (index) => index !== home), next);
|
|
57
|
+
// Every shard degraded: the home shard still owns it (queued, not lost).
|
|
58
|
+
assert.equal(selectShardIndex(key, 3, () => false), home);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('session identity owns the shard and ownership is sticky until released', () => {
|
|
62
|
+
assert.equal(runtimeRoutingKey({ sessionId: 'sess-a' }, 'runtime-1'), 'sess-a');
|
|
63
|
+
assert.equal(runtimeRoutingKey({}, 'runtime-1'), 'runtime-1');
|
|
64
|
+
|
|
65
|
+
const ownership = createShardOwnership();
|
|
66
|
+
const first = ownership.claim('sess-a', () => 1);
|
|
67
|
+
// A second holder of the same session never re-resolves: same shard, even if
|
|
68
|
+
// the placement policy would now choose differently.
|
|
69
|
+
const second = ownership.claim('sess-a', () => 2);
|
|
70
|
+
assert.equal(first, 1);
|
|
71
|
+
assert.equal(second, 1);
|
|
72
|
+
assert.equal(ownership.size, 1);
|
|
73
|
+
ownership.release('sess-a');
|
|
74
|
+
assert.equal(ownership.peek('sess-a'), 1);
|
|
75
|
+
ownership.release('sess-a');
|
|
76
|
+
assert.equal(ownership.peek('sess-a'), null);
|
|
77
|
+
assert.equal(ownership.size, 0);
|
|
78
|
+
assert.equal(ownership.claim('sess-a', () => 2), 2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('provider cooldown merges monotonically across shards', () => {
|
|
82
|
+
const start = { untilMs: 0, disabledReason: null, updatedAt: 0 };
|
|
83
|
+
const first = mergeProviderCooldown(start, { untilMs: 10_000, observedAt: 5 });
|
|
84
|
+
assert.equal(first.changed, true);
|
|
85
|
+
assert.equal(first.cooldown.untilMs, 10_000);
|
|
86
|
+
|
|
87
|
+
// A shorter/echoed cooldown must never shorten or re-broadcast.
|
|
88
|
+
const echo = mergeProviderCooldown(first.cooldown, { untilMs: 9_500 });
|
|
89
|
+
assert.equal(echo.changed, false);
|
|
90
|
+
assert.equal(echo.cooldown.untilMs, 10_000);
|
|
91
|
+
|
|
92
|
+
const longer = mergeProviderCooldown(first.cooldown, { untilMs: 30_000 });
|
|
93
|
+
assert.equal(longer.changed, true);
|
|
94
|
+
assert.equal(longer.cooldown.untilMs, 30_000);
|
|
95
|
+
|
|
96
|
+
const disabled = mergeProviderCooldown(longer.cooldown, { untilMs: 0, disabledReason: 'overage off' });
|
|
97
|
+
assert.equal(disabled.changed, true);
|
|
98
|
+
assert.equal(disabled.cooldown.disabledReason, 'overage off');
|
|
99
|
+
assert.equal(disabled.cooldown.untilMs, 30_000);
|
|
100
|
+
assert.equal(mergeProviderCooldown(disabled.cooldown, { disabledReason: 'overage off' }).changed, false);
|
|
101
|
+
});
|