@kontextmind/kxm 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export const PRICES_SCHEMA = "kxm.prices.v1";
|
|
2
|
+
|
|
3
|
+
export interface PriceTier {
|
|
4
|
+
readonly upToContextTokens?: number | null | undefined;
|
|
5
|
+
readonly inputPerMillion: number;
|
|
6
|
+
readonly outputPerMillion: number;
|
|
7
|
+
readonly cacheReadPerMillion?: number | null | undefined;
|
|
8
|
+
readonly cacheWritePerMillion?: number | null | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ModelPriceRow {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly provider: string;
|
|
14
|
+
readonly model: string;
|
|
15
|
+
readonly aliases?: readonly string[] | undefined;
|
|
16
|
+
readonly tiers: readonly PriceTier[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PriceCatalog {
|
|
20
|
+
readonly schema: typeof PRICES_SCHEMA;
|
|
21
|
+
readonly date: string;
|
|
22
|
+
readonly sha256: string;
|
|
23
|
+
readonly currency?: string | undefined;
|
|
24
|
+
readonly models: readonly ModelPriceRow[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function findModelPrice(catalog: PriceCatalog, model: string, provider?: string): ModelPriceRow | undefined {
|
|
28
|
+
const normalizedModel = model.trim().toLowerCase();
|
|
29
|
+
const normalizedProvider = provider?.trim().toLowerCase();
|
|
30
|
+
|
|
31
|
+
for (const entry of catalog.models) {
|
|
32
|
+
if (normalizedProvider && entry.provider.toLowerCase() !== normalizedProvider) {
|
|
33
|
+
const matchesAlias = entry.aliases?.some((a) => a.toLowerCase() === normalizedModel);
|
|
34
|
+
if (!matchesAlias) continue;
|
|
35
|
+
}
|
|
36
|
+
if (entry.id.toLowerCase() === normalizedModel || entry.model.toLowerCase() === normalizedModel) {
|
|
37
|
+
return entry;
|
|
38
|
+
}
|
|
39
|
+
if (entry.aliases?.some((a) => a.toLowerCase() === normalizedModel)) {
|
|
40
|
+
return entry;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function calculateModelCost(
|
|
47
|
+
catalog: PriceCatalog,
|
|
48
|
+
params: {
|
|
49
|
+
model: string;
|
|
50
|
+
provider?: string;
|
|
51
|
+
tokensIn?: number | null;
|
|
52
|
+
tokensOut?: number | null;
|
|
53
|
+
cacheReadTokens?: number | null;
|
|
54
|
+
cacheWriteTokens?: number | null;
|
|
55
|
+
contextTokens?: number | null;
|
|
56
|
+
},
|
|
57
|
+
): { costUsd: number; priceRef: string } | undefined {
|
|
58
|
+
const row = findModelPrice(catalog, params.model, params.provider);
|
|
59
|
+
if (!row || row.tiers.length === 0) return undefined;
|
|
60
|
+
|
|
61
|
+
const context = params.contextTokens ?? params.tokensIn ?? 0;
|
|
62
|
+
let selectedTier = row.tiers[0]!;
|
|
63
|
+
for (const tier of row.tiers) {
|
|
64
|
+
if (tier.upToContextTokens !== undefined && tier.upToContextTokens !== null && context > tier.upToContextTokens) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
selectedTier = tier;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const tokensIn = params.tokensIn ?? 0;
|
|
72
|
+
const tokensOut = params.tokensOut ?? 0;
|
|
73
|
+
const cacheRead = params.cacheReadTokens ?? 0;
|
|
74
|
+
const cacheWrite = params.cacheWriteTokens ?? 0;
|
|
75
|
+
|
|
76
|
+
const cost =
|
|
77
|
+
(tokensIn / 1_000_000) * selectedTier.inputPerMillion +
|
|
78
|
+
(tokensOut / 1_000_000) * selectedTier.outputPerMillion +
|
|
79
|
+
(cacheRead / 1_000_000) * (selectedTier.cacheReadPerMillion ?? 0) +
|
|
80
|
+
(cacheWrite / 1_000_000) * (selectedTier.cacheWritePerMillion ?? 0);
|
|
81
|
+
|
|
82
|
+
const priceRef = `${catalog.date}#${row.id}`;
|
|
83
|
+
return {
|
|
84
|
+
costUsd: Math.round(cost * 1_000_000) / 1_000_000,
|
|
85
|
+
priceRef,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parse } from "yaml";
|
|
5
|
+
|
|
6
|
+
export * from "./price-calc.ts";
|
|
7
|
+
import type { PriceCatalog, ModelPriceRow, PriceTier } from "./price-calc.ts";
|
|
8
|
+
import { PRICES_SCHEMA } from "./price-calc.ts";
|
|
9
|
+
|
|
10
|
+
export function hashPriceCatalog(catalog: Omit<PriceCatalog, "sha256">): string {
|
|
11
|
+
const canonical = {
|
|
12
|
+
schema: catalog.schema,
|
|
13
|
+
date: catalog.date,
|
|
14
|
+
currency: catalog.currency ?? "USD",
|
|
15
|
+
models: catalog.models.map((m) => ({
|
|
16
|
+
id: m.id,
|
|
17
|
+
provider: m.provider,
|
|
18
|
+
model: m.model,
|
|
19
|
+
aliases: m.aliases ? [...m.aliases].sort() : [],
|
|
20
|
+
tiers: m.tiers.map((t) => ({
|
|
21
|
+
upToContextTokens: t.upToContextTokens ?? null,
|
|
22
|
+
inputPerMillion: t.inputPerMillion,
|
|
23
|
+
outputPerMillion: t.outputPerMillion,
|
|
24
|
+
cacheReadPerMillion: t.cacheReadPerMillion ?? null,
|
|
25
|
+
cacheWritePerMillion: t.cacheWritePerMillion ?? null,
|
|
26
|
+
})),
|
|
27
|
+
})).sort((a, b) => a.id.localeCompare(b.id)),
|
|
28
|
+
};
|
|
29
|
+
return createHash("sha256").update(JSON.stringify(canonical), "utf8").digest("hex");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parsePriceCatalog(text: string): PriceCatalog {
|
|
33
|
+
const parsed = parse(text) as Record<string, unknown>;
|
|
34
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
35
|
+
throw new Error("price catalog must be an object");
|
|
36
|
+
}
|
|
37
|
+
if (parsed.schema !== PRICES_SCHEMA) {
|
|
38
|
+
throw new Error(`price catalog schema must be ${PRICES_SCHEMA}`);
|
|
39
|
+
}
|
|
40
|
+
if (typeof parsed.date !== "string" || !/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(parsed.date)) {
|
|
41
|
+
throw new Error("price catalog date must be YYYY-MM-DD");
|
|
42
|
+
}
|
|
43
|
+
if (typeof parsed.sha256 !== "string" || !/^(?:sha256:)?[a-f0-9]{64}$/.test(parsed.sha256)) {
|
|
44
|
+
throw new Error("price catalog sha256 must be a 64-character hex digest");
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(parsed.models) || parsed.models.length === 0) {
|
|
47
|
+
throw new Error("price catalog models must be a non-empty array");
|
|
48
|
+
}
|
|
49
|
+
const models: ModelPriceRow[] = parsed.models.map((item, index) => {
|
|
50
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
51
|
+
throw new Error(`price catalog model at index ${index} must be an object`);
|
|
52
|
+
}
|
|
53
|
+
const m = item as Record<string, unknown>;
|
|
54
|
+
if (typeof m.id !== "string" || !m.id) {
|
|
55
|
+
throw new Error(`price catalog model at index ${index} must have a non-empty id`);
|
|
56
|
+
}
|
|
57
|
+
if (typeof m.provider !== "string" || !m.provider) {
|
|
58
|
+
throw new Error(`price catalog model at index ${index} must have a non-empty provider`);
|
|
59
|
+
}
|
|
60
|
+
if (typeof m.model !== "string" || !m.model) {
|
|
61
|
+
throw new Error(`price catalog model at index ${index} must have a non-empty model`);
|
|
62
|
+
}
|
|
63
|
+
const aliases = Array.isArray(m.aliases)
|
|
64
|
+
? m.aliases.filter((a): a is string => typeof a === "string" && Boolean(a))
|
|
65
|
+
: undefined;
|
|
66
|
+
if (!Array.isArray(m.tiers) || m.tiers.length === 0) {
|
|
67
|
+
throw new Error(`price catalog model ${m.id} must have at least one tier`);
|
|
68
|
+
}
|
|
69
|
+
const tiers: PriceTier[] = m.tiers.map((tItem, tIndex) => {
|
|
70
|
+
if (!tItem || typeof tItem !== "object" || Array.isArray(tItem)) {
|
|
71
|
+
throw new Error(`tier at index ${tIndex} for model ${m.id} must be an object`);
|
|
72
|
+
}
|
|
73
|
+
const t = tItem as Record<string, unknown>;
|
|
74
|
+
const inputRate = typeof t.inputPerMillion === "number" ? t.inputPerMillion : (typeof t.input === "number" ? t.input : undefined);
|
|
75
|
+
const outputRate = typeof t.outputPerMillion === "number" ? t.outputPerMillion : (typeof t.output === "number" ? t.output : undefined);
|
|
76
|
+
if (inputRate === undefined || inputRate < 0) {
|
|
77
|
+
throw new Error(`tier at index ${tIndex} for model ${m.id} must have a non-negative inputPerMillion`);
|
|
78
|
+
}
|
|
79
|
+
if (outputRate === undefined || outputRate < 0) {
|
|
80
|
+
throw new Error(`tier at index ${tIndex} for model ${m.id} must have a non-negative outputPerMillion`);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
upToContextTokens: typeof t.upToContextTokens === "number" ? t.upToContextTokens : null,
|
|
84
|
+
inputPerMillion: inputRate,
|
|
85
|
+
outputPerMillion: outputRate,
|
|
86
|
+
cacheReadPerMillion: typeof t.cacheReadPerMillion === "number" ? t.cacheReadPerMillion : (typeof t.cacheRead === "number" ? t.cacheRead : null),
|
|
87
|
+
cacheWritePerMillion: typeof t.cacheWritePerMillion === "number" ? t.cacheWritePerMillion : (typeof t.cacheWrite === "number" ? t.cacheWrite : null),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
id: m.id,
|
|
92
|
+
provider: m.provider,
|
|
93
|
+
model: m.model,
|
|
94
|
+
aliases,
|
|
95
|
+
tiers,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
schema: PRICES_SCHEMA,
|
|
101
|
+
date: parsed.date,
|
|
102
|
+
sha256: parsed.sha256.replace(/^sha256:/, ""),
|
|
103
|
+
currency: typeof parsed.currency === "string" ? parsed.currency : "USD",
|
|
104
|
+
models,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function loadPriceCatalog(rootOrPath: string): PriceCatalog | undefined {
|
|
109
|
+
const candidatePath = existsSync(join(rootOrPath, ".kxm", "prices.yaml"))
|
|
110
|
+
? join(rootOrPath, ".kxm", "prices.yaml")
|
|
111
|
+
: (existsSync(join(rootOrPath, "prices.yaml"))
|
|
112
|
+
? join(rootOrPath, "prices.yaml")
|
|
113
|
+
: (existsSync(rootOrPath) && !rootOrPath.endsWith("/") ? rootOrPath : undefined));
|
|
114
|
+
|
|
115
|
+
if (!candidatePath || !existsSync(candidatePath)) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const content = readFileSync(candidatePath, "utf8");
|
|
119
|
+
return parsePriceCatalog(content);
|
|
120
|
+
}
|
|
121
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_PORT = 7331;
|
|
4
|
+
export const DEFAULT_STALE_AFTER_MS = 30_000;
|
|
5
|
+
export const DEFAULT_MAX_HOPS = 5;
|
|
6
|
+
export const DEFAULT_MESSAGE_TTL_MS = 24 * 60 * 60_000;
|
|
7
|
+
export const MIN_MESSAGE_TTL_MS = 1_000;
|
|
8
|
+
export const MAX_MESSAGE_TTL_MS = 7 * 24 * 60 * 60_000;
|
|
9
|
+
export const DEFAULT_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 60_000;
|
|
10
|
+
export const MIN_MESSAGE_RETENTION_MS = 1_000;
|
|
11
|
+
export const DEFAULT_RATE_LIMIT_MAX = 600;
|
|
12
|
+
export const DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
13
|
+
export const MAX_BODY_BYTES = 256 * 1024;
|
|
14
|
+
export const MAX_CONTENT_CHARS = 32_000;
|
|
15
|
+
|
|
16
|
+
export type DeliveryMode = "steer" | "followUp" | "nextTurn";
|
|
17
|
+
export type MessageStatus = "queued" | "delivered" | "replied" | "cancelled" | "expired" | "error";
|
|
18
|
+
|
|
19
|
+
export interface AgentRecord {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
purpose: string;
|
|
23
|
+
project: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
connectedAt: string;
|
|
26
|
+
lastSeenAt: string;
|
|
27
|
+
online: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MessageReply {
|
|
31
|
+
content: string;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Hub-authorized scope for a peer request that may later be cited as
|
|
36
|
+
* workflow evidence. Callers request this scope, but the hub validates it
|
|
37
|
+
* against the active run and persists the canonical value. */
|
|
38
|
+
export interface WorkflowMessageContext {
|
|
39
|
+
schema: "pi-mesh.workflow-message-context.v1";
|
|
40
|
+
runId: string;
|
|
41
|
+
stageId: string;
|
|
42
|
+
requirementKey: string;
|
|
43
|
+
attempt: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type WorkflowCheckpointStatus = "passed" | "warning" | "failed";
|
|
47
|
+
export type JournalCategory =
|
|
48
|
+
| "plan"
|
|
49
|
+
| "decision"
|
|
50
|
+
| "contradiction"
|
|
51
|
+
| "error"
|
|
52
|
+
| "lesson"
|
|
53
|
+
| "observation"
|
|
54
|
+
| "hypothesis"
|
|
55
|
+
| "experiment"
|
|
56
|
+
| "state-change"
|
|
57
|
+
| "skill-candidate";
|
|
58
|
+
export type ImprovementArea = "harness" | "gates" | "implementation" | "workflow" | "documentation" | "security" | "other";
|
|
59
|
+
|
|
60
|
+
/** Evidence submitted for one checkpoint or external signal, keyed by a
|
|
61
|
+
* requirement from WorkflowStageDefinition.requiredEvidence. */
|
|
62
|
+
export type WorkflowEvidenceInput = Record<string, string>;
|
|
63
|
+
|
|
64
|
+
/** Callers cite durable message IDs only. Producer identity and the evidence
|
|
65
|
+
* snapshot are derived by the hub from the stored message. */
|
|
66
|
+
export interface WorkflowEvidenceReference {
|
|
67
|
+
messageIds: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type WorkflowEvidenceReferenceInput = Record<string, WorkflowEvidenceReference>;
|
|
71
|
+
|
|
72
|
+
export interface MessageRecord {
|
|
73
|
+
id: string;
|
|
74
|
+
project: string;
|
|
75
|
+
from: string;
|
|
76
|
+
fromName: string;
|
|
77
|
+
to: string;
|
|
78
|
+
toName: string;
|
|
79
|
+
content: string;
|
|
80
|
+
delivery: DeliveryMode;
|
|
81
|
+
hops: number;
|
|
82
|
+
maxHops: number;
|
|
83
|
+
correlationId?: string;
|
|
84
|
+
replyTo?: string;
|
|
85
|
+
idempotencyKey?: string;
|
|
86
|
+
seq?: number;
|
|
87
|
+
/** Hub-owned durable workflow affinity. Callers cannot establish this field
|
|
88
|
+
* directly; the hub derives it from an authorized workflow context or an
|
|
89
|
+
* internally-created workflow prompt. */
|
|
90
|
+
workflowRunId?: string;
|
|
91
|
+
workflowContext?: WorkflowMessageContext;
|
|
92
|
+
createdAt: string;
|
|
93
|
+
expiresAt: string;
|
|
94
|
+
deliveredAt?: string;
|
|
95
|
+
repliedAt?: string;
|
|
96
|
+
cancelledAt?: string;
|
|
97
|
+
status: MessageStatus;
|
|
98
|
+
reply?: MessageReply;
|
|
99
|
+
error?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type HubEvent =
|
|
103
|
+
| { type: "message"; message: MessageRecord }
|
|
104
|
+
| { type: "reply"; message: MessageRecord }
|
|
105
|
+
| { type: "cancelled"; message: MessageRecord }
|
|
106
|
+
| { type: "expired"; message: MessageRecord }
|
|
107
|
+
| { type: "presence"; agent: AgentRecord };
|
|
108
|
+
|
|
109
|
+
export class ProtocolError extends Error {
|
|
110
|
+
readonly statusCode: number;
|
|
111
|
+
readonly code: string;
|
|
112
|
+
readonly extras?: Record<string, unknown>;
|
|
113
|
+
|
|
114
|
+
constructor(statusCode: number, message: string, code = "protocol_error", extras?: Record<string, unknown>) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.name = "ProtocolError";
|
|
117
|
+
this.statusCode = statusCode;
|
|
118
|
+
this.code = code;
|
|
119
|
+
if (extras) this.extras = extras;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function nowIso(): string {
|
|
124
|
+
return new Date().toISOString();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function newId(prefix: string): string {
|
|
128
|
+
return `${prefix}_${randomUUID().replaceAll("-", "")}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function requireString(
|
|
132
|
+
value: unknown,
|
|
133
|
+
field: string,
|
|
134
|
+
options: { max?: number; allowEmpty?: boolean } = {},
|
|
135
|
+
): string {
|
|
136
|
+
if (typeof value !== "string") {
|
|
137
|
+
throw new ProtocolError(400, `${field} must be a string`);
|
|
138
|
+
}
|
|
139
|
+
const result = value.trim();
|
|
140
|
+
if (!options.allowEmpty && result.length === 0) {
|
|
141
|
+
throw new ProtocolError(400, `${field} cannot be empty`);
|
|
142
|
+
}
|
|
143
|
+
if (options.max !== undefined && result.length > options.max) {
|
|
144
|
+
throw new ProtocolError(400, `${field} exceeds ${options.max} characters`);
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function optionalString(value: unknown, field: string, max: number): string | undefined {
|
|
150
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
151
|
+
return requireString(value, field, { max });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function parseDeliveryMode(value: unknown): DeliveryMode {
|
|
155
|
+
if (value === undefined) return "followUp";
|
|
156
|
+
if (value === "steer" || value === "followUp" || value === "nextTurn") return value;
|
|
157
|
+
throw new ProtocolError(400, "delivery must be steer, followUp, or nextTurn");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function parseBoundedInteger(
|
|
161
|
+
value: unknown,
|
|
162
|
+
field: string,
|
|
163
|
+
fallback: number,
|
|
164
|
+
min: number,
|
|
165
|
+
max: number,
|
|
166
|
+
): number {
|
|
167
|
+
if (value === undefined) return fallback;
|
|
168
|
+
if (!Number.isInteger(value) || (value as number) < min || (value as number) > max) {
|
|
169
|
+
throw new ProtocolError(400, `${field} must be an integer between ${min} and ${max}`);
|
|
170
|
+
}
|
|
171
|
+
return value as number;
|
|
172
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { HubHttpError, type HubClient } from "./client.ts";
|
|
5
|
+
import { classifyFailure, diagnosticEvidence } from "./diagnostics.ts";
|
|
6
|
+
|
|
7
|
+
export interface WorkerRecoveryEnvelope {
|
|
8
|
+
version: 1;
|
|
9
|
+
reason: "missing_tool_result" | "unresumable_session" | "provider_error" | "tool_timeout" | "worker_signal";
|
|
10
|
+
agentName: string;
|
|
11
|
+
project: string;
|
|
12
|
+
workerKey?: string;
|
|
13
|
+
generation?: string;
|
|
14
|
+
previousContinue: boolean;
|
|
15
|
+
freshSession: boolean;
|
|
16
|
+
sessionBinding?: { kind: "default" } | { kind: "workflow"; runId: string };
|
|
17
|
+
failureClass?: "quota" | "provider_error" | "timeout";
|
|
18
|
+
signal?: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
runId?: string | null;
|
|
21
|
+
stageId?: string | null;
|
|
22
|
+
activeMessageIds?: string[];
|
|
23
|
+
pendingMessageIds?: string[];
|
|
24
|
+
artifactPointers?: string[];
|
|
25
|
+
/** Runtime-only disposition; never written by the supervisor. */
|
|
26
|
+
peerLocal?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function workerStateKey(project: string, agentName: string): string {
|
|
30
|
+
const safeProject = project.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 24) || "project";
|
|
31
|
+
const safeName = agentName.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 32) || "agent";
|
|
32
|
+
const digest = createHash("sha256")
|
|
33
|
+
.update(JSON.stringify({ project, agentName }))
|
|
34
|
+
.digest("hex")
|
|
35
|
+
.slice(0, 24);
|
|
36
|
+
return `${safeProject}-${safeName}-${digest}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function legacyRecoveryEnvelopePath(stateDir: string, agentName: string): string {
|
|
40
|
+
const safeName = agentName.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
41
|
+
return join(stateDir, `worker-recovery-${safeName}.json`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function recoveryEnvelopePath(stateDir: string, agentName: string, project?: string): string {
|
|
45
|
+
return project
|
|
46
|
+
? join(stateDir, `worker-recovery-${workerStateKey(project, agentName)}.json`)
|
|
47
|
+
: legacyRecoveryEnvelopePath(stateDir, agentName);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const recoveryReasons = new Set<WorkerRecoveryEnvelope["reason"]>([
|
|
51
|
+
"missing_tool_result", "unresumable_session", "provider_error", "tool_timeout", "worker_signal",
|
|
52
|
+
]);
|
|
53
|
+
const recoveryRunId = /^run_[a-f0-9]{32}$/;
|
|
54
|
+
const recoveryMessageId = /^msg_[a-f0-9]{32}$/;
|
|
55
|
+
|
|
56
|
+
function validBoundedStrings(value: unknown, maxItems: number, pattern?: RegExp): value is string[] {
|
|
57
|
+
return Array.isArray(value)
|
|
58
|
+
&& value.length <= maxItems
|
|
59
|
+
&& value.every((item) => typeof item === "string" && item.length > 0 && item.length <= 2_048 && !/[\0\r\n]/.test(item) && (!pattern || pattern.test(item)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseRecoveryEnvelope(value: unknown, agentName: string, project?: string): WorkerRecoveryEnvelope {
|
|
63
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("recovery envelope must be an object");
|
|
64
|
+
const parsed = value as Partial<WorkerRecoveryEnvelope>;
|
|
65
|
+
if (
|
|
66
|
+
parsed.version !== 1
|
|
67
|
+
|| !recoveryReasons.has(parsed.reason as WorkerRecoveryEnvelope["reason"])
|
|
68
|
+
|| parsed.agentName !== agentName
|
|
69
|
+
|| typeof parsed.project !== "string"
|
|
70
|
+
|| parsed.project.length < 1
|
|
71
|
+
|| parsed.project.length > 128
|
|
72
|
+
|| (project !== undefined && parsed.project !== project)
|
|
73
|
+
|| typeof parsed.previousContinue !== "boolean"
|
|
74
|
+
|| typeof parsed.freshSession !== "boolean"
|
|
75
|
+
|| typeof parsed.createdAt !== "string"
|
|
76
|
+
|| !Number.isFinite(Date.parse(parsed.createdAt))
|
|
77
|
+
) throw new Error("recovery envelope identity or schema is invalid");
|
|
78
|
+
if (parsed.runId !== undefined && parsed.runId !== null && !recoveryRunId.test(parsed.runId)) {
|
|
79
|
+
throw new Error("recovery envelope run identity is invalid");
|
|
80
|
+
}
|
|
81
|
+
if (parsed.stageId !== undefined && parsed.stageId !== null && (typeof parsed.stageId !== "string" || !/^[A-Za-z0-9_.-]{1,128}$/.test(parsed.stageId))) {
|
|
82
|
+
throw new Error("recovery envelope stage identity is invalid");
|
|
83
|
+
}
|
|
84
|
+
if (parsed.activeMessageIds !== undefined && !validBoundedStrings(parsed.activeMessageIds, 3, recoveryMessageId)) {
|
|
85
|
+
throw new Error("recovery envelope active messages are invalid");
|
|
86
|
+
}
|
|
87
|
+
if (parsed.pendingMessageIds !== undefined && !validBoundedStrings(parsed.pendingMessageIds, 16, recoveryMessageId)) {
|
|
88
|
+
throw new Error("recovery envelope pending messages are invalid");
|
|
89
|
+
}
|
|
90
|
+
if (parsed.artifactPointers !== undefined && !validBoundedStrings(parsed.artifactPointers, 16)) {
|
|
91
|
+
throw new Error("recovery envelope artifact pointers are invalid");
|
|
92
|
+
}
|
|
93
|
+
if (parsed.failureClass !== undefined && parsed.failureClass !== "quota" && parsed.failureClass !== "provider_error" && parsed.failureClass !== "timeout") {
|
|
94
|
+
throw new Error("recovery envelope failure class is invalid");
|
|
95
|
+
}
|
|
96
|
+
if (parsed.signal !== undefined && (typeof parsed.signal !== "string" || parsed.signal.length > 64 || /[\0\r\n]/.test(parsed.signal))) {
|
|
97
|
+
throw new Error("recovery envelope signal is invalid");
|
|
98
|
+
}
|
|
99
|
+
if (parsed.sessionBinding !== undefined) {
|
|
100
|
+
const binding = parsed.sessionBinding;
|
|
101
|
+
if (
|
|
102
|
+
!binding
|
|
103
|
+
|| typeof binding !== "object"
|
|
104
|
+
|| (binding.kind === "default" && Object.keys(binding).some((key) => key !== "kind"))
|
|
105
|
+
|| (binding.kind === "workflow" && !recoveryRunId.test(binding.runId))
|
|
106
|
+
|| (binding.kind !== "default" && binding.kind !== "workflow")
|
|
107
|
+
) throw new Error("recovery envelope session binding is invalid");
|
|
108
|
+
}
|
|
109
|
+
return parsed as WorkerRecoveryEnvelope;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findWorkerRecoveryEnvelope(
|
|
113
|
+
stateDir: string,
|
|
114
|
+
agentName: string,
|
|
115
|
+
project?: string,
|
|
116
|
+
): { envelope: WorkerRecoveryEnvelope; path: string } | undefined {
|
|
117
|
+
const candidates = project
|
|
118
|
+
? [
|
|
119
|
+
{ path: recoveryEnvelopePath(stateDir, agentName, project), quarantineInvalid: true },
|
|
120
|
+
{ path: legacyRecoveryEnvelopePath(stateDir, agentName), quarantineInvalid: false },
|
|
121
|
+
]
|
|
122
|
+
: [{ path: legacyRecoveryEnvelopePath(stateDir, agentName), quarantineInvalid: false }];
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
try {
|
|
125
|
+
const stats = lstatSync(candidate.path);
|
|
126
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.size > 128 * 1_024) {
|
|
127
|
+
throw new Error("recovery envelope is not a bounded regular file");
|
|
128
|
+
}
|
|
129
|
+
const envelope = parseRecoveryEnvelope(JSON.parse(readFileSync(candidate.path, "utf8")), agentName, project);
|
|
130
|
+
return { envelope, path: candidate.path };
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") continue;
|
|
133
|
+
if (candidate.quarantineInvalid) {
|
|
134
|
+
try {
|
|
135
|
+
renameSync(candidate.path, `${candidate.path}.corrupt-${Date.now()}`);
|
|
136
|
+
} catch {
|
|
137
|
+
// Failure to quarantine leaves the envelope untouched for an operator.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Try the legacy path after an absent or invalid collision-safe path.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function readWorkerRecoveryEnvelope(stateDir: string, agentName: string, project?: string): WorkerRecoveryEnvelope | undefined {
|
|
147
|
+
return findWorkerRecoveryEnvelope(stateDir, agentName, project)?.envelope;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function consumeWorkerRecoveryEnvelope(client: HubClient, stateDir: string, agentName: string, project?: string): Promise<WorkerRecoveryEnvelope | undefined> {
|
|
151
|
+
const found = findWorkerRecoveryEnvelope(stateDir, agentName, project);
|
|
152
|
+
if (!found) return undefined;
|
|
153
|
+
const { envelope, path } = found;
|
|
154
|
+
let runId = envelope.runId ?? undefined;
|
|
155
|
+
let stageId = envelope.stageId ?? undefined;
|
|
156
|
+
let scopeMismatch = false;
|
|
157
|
+
if (envelope.sessionBinding) {
|
|
158
|
+
const boundRunId = envelope.sessionBinding.kind === "workflow" ? envelope.sessionBinding.runId : undefined;
|
|
159
|
+
if (runId !== boundRunId) {
|
|
160
|
+
// Never attach recovery from a default or sibling Pi session to this run.
|
|
161
|
+
scopeMismatch = true;
|
|
162
|
+
runId = undefined;
|
|
163
|
+
stageId = undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!runId) {
|
|
167
|
+
// Recovery provenance is exact: an unbound worker event must never be
|
|
168
|
+
// attached to whichever workflow happens to be active in the project.
|
|
169
|
+
rmSync(path, { force: true });
|
|
170
|
+
return scopeMismatch ? { ...envelope, runId: null, stageId: null } : envelope;
|
|
171
|
+
}
|
|
172
|
+
const providerRecovery = envelope.reason === "provider_error";
|
|
173
|
+
const diagnostic = classifyFailure({
|
|
174
|
+
toolName: providerRecovery ? "provider" : envelope.reason === "tool_timeout" ? "tool" : "kxm_await",
|
|
175
|
+
...(providerRecovery
|
|
176
|
+
? { code: envelope.failureClass === "quota" ? "provider_quota" : "provider_error" }
|
|
177
|
+
: envelope.reason === "tool_timeout" ? {} : { code: "unresumable_session" }),
|
|
178
|
+
message: envelope.reason === "tool_timeout" ? "timed out" : envelope.reason,
|
|
179
|
+
});
|
|
180
|
+
try {
|
|
181
|
+
await client.recordWorkflowEntry(runId, {
|
|
182
|
+
category: "error",
|
|
183
|
+
area: "harness",
|
|
184
|
+
severity: "error",
|
|
185
|
+
summary: `Worker recovered with ${envelope.reason}`,
|
|
186
|
+
evidence: [
|
|
187
|
+
...diagnosticEvidence(diagnostic),
|
|
188
|
+
`recovery:v1`,
|
|
189
|
+
`reason:${envelope.reason}`,
|
|
190
|
+
...(envelope.sessionBinding?.kind === "workflow" ? [`session-scope:${envelope.sessionBinding.runId}`] : []),
|
|
191
|
+
...(stageId ? [`stage:${stageId}`] : []),
|
|
192
|
+
...(envelope.pendingMessageIds ?? []).slice(0, 8).map((id) => `message:${id}`),
|
|
193
|
+
...(envelope.artifactPointers ?? []).slice(0, 16).map((pointer) => pointer.startsWith("artifact:") ? pointer : `artifact:${pointer}`),
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
rmSync(path, { force: true });
|
|
197
|
+
return { ...envelope, runId, ...(stageId ? { stageId } : {}) };
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (error instanceof HubHttpError && error.statusCode === 403 && error.code === "workflow_forbidden") {
|
|
200
|
+
// Workflow-affine peers need the same local replay protection as the
|
|
201
|
+
// coordinator, but only the assigned coordinator may mutate the journal.
|
|
202
|
+
// Consume their local envelope instead of retrying an unauthorized write
|
|
203
|
+
// forever; the hub's durable message remains the recovery authority.
|
|
204
|
+
rmSync(path, { force: true });
|
|
205
|
+
return { ...envelope, runId, ...(stageId ? { stageId } : {}), peerLocal: true };
|
|
206
|
+
}
|
|
207
|
+
// Keep the envelope if an authorized journal write failed transiently so
|
|
208
|
+
// the next session can retry it.
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const SECRET_PATTERNS: RegExp[] = [
|
|
2
|
+
/\bsk-[A-Za-z0-9_-]{8,}\b/g,
|
|
3
|
+
/\bghp_[A-Za-z0-9_]{20,}\b/g,
|
|
4
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
5
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/gi,
|
|
6
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]+\b/gi,
|
|
7
|
+
/\bKXM_[A-Z0-9_]*(TOKEN|SECRET|KEY)[A-Z0-9_]*=\S+/gi,
|
|
8
|
+
/\b(GITHUB_TOKEN|GH_TOKEN|KXM_AUTH_TOKEN|KXM_WORKFLOW_SIGNAL_SECRET)=\S+/gi,
|
|
9
|
+
/\b[A-Fa-f0-9]{64}\b/g,
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export function redactSecrets(value: string): string {
|
|
13
|
+
let result = value;
|
|
14
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
15
|
+
result = result.replace(pattern, "[redacted]");
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function looksLikeSecret(value: string): boolean {
|
|
21
|
+
return redactSecrets(value) !== value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function redactStringList(values: string[], maxItems = 32): string[] {
|
|
25
|
+
return values.slice(0, maxItems).map((value) => redactSecrets(value).slice(0, 500));
|
|
26
|
+
}
|