@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,3797 @@
|
|
|
1
|
+
// plugins/kxm/src/extension.ts
|
|
2
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
3
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4
|
+
import { basename, dirname as dirname2, join as join6 } from "node:path";
|
|
5
|
+
|
|
6
|
+
// plugins/kxm/src/client.ts
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
// plugins/kxm/src/protocol.ts
|
|
10
|
+
var DEFAULT_MESSAGE_TTL_MS = 24 * 60 * 6e4;
|
|
11
|
+
var MAX_MESSAGE_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
12
|
+
var DEFAULT_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 6e4;
|
|
13
|
+
var MAX_BODY_BYTES = 256 * 1024;
|
|
14
|
+
var MAX_CONTENT_CHARS = 32e3;
|
|
15
|
+
|
|
16
|
+
// plugins/kxm/src/workflow.ts
|
|
17
|
+
function canonicalWorkflowEvidenceKey(value) {
|
|
18
|
+
return value.trim().replace(/\s+/gu, " ").toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// plugins/kxm/src/client.ts
|
|
22
|
+
var MeshWaitError = class extends Error {
|
|
23
|
+
waitStatus;
|
|
24
|
+
constructor(waitStatus, messageId) {
|
|
25
|
+
super(waitStatus === "aborted" ? "await cancelled" : `timed out waiting for ${messageId}`);
|
|
26
|
+
this.name = "MeshWaitError";
|
|
27
|
+
this.waitStatus = waitStatus;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function completedFanoutResult(target, message) {
|
|
31
|
+
if (message.status === "queued" || message.status === "delivered") {
|
|
32
|
+
throw new Error(`message ${message.id} is not complete`);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
target,
|
|
36
|
+
messageId: message.id,
|
|
37
|
+
status: message.status,
|
|
38
|
+
...message.reply ? { reply: message.reply.content } : {},
|
|
39
|
+
...message.error ? { error: message.error } : {}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function fanoutIdempotencyKey(prefix, target, correlationId, workflowContext) {
|
|
43
|
+
const scope = JSON.stringify(workflowContext ? {
|
|
44
|
+
prefix,
|
|
45
|
+
correlationId: correlationId ?? null,
|
|
46
|
+
target: target.toLowerCase(),
|
|
47
|
+
workflowContext: {
|
|
48
|
+
runId: workflowContext.runId,
|
|
49
|
+
stageId: workflowContext.stageId,
|
|
50
|
+
requirementKey: canonicalWorkflowEvidenceKey(workflowContext.requirementKey),
|
|
51
|
+
attempt: workflowContext.attempt
|
|
52
|
+
}
|
|
53
|
+
} : {
|
|
54
|
+
prefix,
|
|
55
|
+
correlationId: correlationId ?? null,
|
|
56
|
+
target: target.toLowerCase()
|
|
57
|
+
});
|
|
58
|
+
return `fanout:${createHash("sha256").update(scope).digest("hex")}`;
|
|
59
|
+
}
|
|
60
|
+
var HubHttpError = class extends Error {
|
|
61
|
+
statusCode;
|
|
62
|
+
code;
|
|
63
|
+
requestId;
|
|
64
|
+
extras;
|
|
65
|
+
constructor(statusCode, message, code, requestId, extras) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "HubHttpError";
|
|
68
|
+
this.statusCode = statusCode;
|
|
69
|
+
if (code) this.code = code;
|
|
70
|
+
if (requestId) this.requestId = requestId;
|
|
71
|
+
if (extras) this.extras = extras;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var HubClient = class {
|
|
75
|
+
options;
|
|
76
|
+
agent;
|
|
77
|
+
agentKey;
|
|
78
|
+
heartbeatTimer;
|
|
79
|
+
eventsAbort;
|
|
80
|
+
stopped = true;
|
|
81
|
+
onEvent;
|
|
82
|
+
registration;
|
|
83
|
+
eventLoop;
|
|
84
|
+
constructor(options) {
|
|
85
|
+
this.options = { heartbeatMs: 1e4, reconnectMs: 1e3, requestTimeoutMs: 15e3, ...options };
|
|
86
|
+
}
|
|
87
|
+
async start(onEvent) {
|
|
88
|
+
if (!this.stopped) throw new Error("hub client is already started");
|
|
89
|
+
this.stopped = false;
|
|
90
|
+
this.onEvent = onEvent;
|
|
91
|
+
try {
|
|
92
|
+
await this.register();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
this.stopped = true;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
this.heartbeatTimer = setInterval(() => void this.heartbeat(), this.options.heartbeatMs);
|
|
98
|
+
this.heartbeatTimer.unref();
|
|
99
|
+
this.eventLoop = this.runEventLoop();
|
|
100
|
+
return this.agent;
|
|
101
|
+
}
|
|
102
|
+
async stop() {
|
|
103
|
+
if (this.stopped) return;
|
|
104
|
+
this.stopped = true;
|
|
105
|
+
this.eventsAbort?.abort();
|
|
106
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
107
|
+
await this.eventLoop;
|
|
108
|
+
if (this.agent) {
|
|
109
|
+
try {
|
|
110
|
+
await this.request(`/v1/agents/${encodeURIComponent(this.agent.id)}`, { method: "DELETE" });
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
this.agent = void 0;
|
|
115
|
+
this.agentKey = void 0;
|
|
116
|
+
this.onEvent = void 0;
|
|
117
|
+
this.eventLoop = void 0;
|
|
118
|
+
}
|
|
119
|
+
async listAgents() {
|
|
120
|
+
const result2 = await this.request("/v1/agents");
|
|
121
|
+
return result2.agents;
|
|
122
|
+
}
|
|
123
|
+
async send(options) {
|
|
124
|
+
const result2 = await this.request("/v1/messages", {
|
|
125
|
+
method: "POST",
|
|
126
|
+
body: JSON.stringify(options)
|
|
127
|
+
});
|
|
128
|
+
return result2.message;
|
|
129
|
+
}
|
|
130
|
+
async fanout(options) {
|
|
131
|
+
const targets = [...new Set(options.targets.map((target) => target.trim().toLowerCase()).filter(Boolean))];
|
|
132
|
+
if (targets.length < 1 || targets.length > 3) throw new Error("fanout requires between one and three unique targets");
|
|
133
|
+
return await Promise.all(targets.map(async (target) => {
|
|
134
|
+
let message;
|
|
135
|
+
try {
|
|
136
|
+
message = await this.send({
|
|
137
|
+
target,
|
|
138
|
+
content: options.content,
|
|
139
|
+
delivery: "followUp",
|
|
140
|
+
...options.correlationId ? { correlationId: options.correlationId } : {},
|
|
141
|
+
...options.workflowContext ? { workflowContext: options.workflowContext } : {},
|
|
142
|
+
...options.idempotencyKeyPrefix ? {
|
|
143
|
+
idempotencyKey: fanoutIdempotencyKey(
|
|
144
|
+
options.idempotencyKeyPrefix,
|
|
145
|
+
target,
|
|
146
|
+
options.correlationId,
|
|
147
|
+
options.workflowContext
|
|
148
|
+
)
|
|
149
|
+
} : {},
|
|
150
|
+
...options.ttlMs ? { ttlMs: options.ttlMs } : {}
|
|
151
|
+
});
|
|
152
|
+
const completed = await this.awaitResponse(
|
|
153
|
+
message.id,
|
|
154
|
+
options.timeoutMs ?? 30 * 6e4,
|
|
155
|
+
options.signal
|
|
156
|
+
);
|
|
157
|
+
return completedFanoutResult(target, completed);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (message && error instanceof MeshWaitError) {
|
|
160
|
+
try {
|
|
161
|
+
const current = await this.getMessage(message.id);
|
|
162
|
+
if (current.status === "queued" || current.status === "delivered") {
|
|
163
|
+
return {
|
|
164
|
+
target,
|
|
165
|
+
messageId: current.id,
|
|
166
|
+
status: "pending",
|
|
167
|
+
messageStatus: current.status,
|
|
168
|
+
expiresAt: current.expiresAt,
|
|
169
|
+
waitStatus: error.waitStatus
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return completedFanoutResult(target, current);
|
|
173
|
+
} catch (finalError) {
|
|
174
|
+
return {
|
|
175
|
+
target,
|
|
176
|
+
messageId: message.id,
|
|
177
|
+
status: "error",
|
|
178
|
+
error: finalError instanceof Error ? finalError.message : String(finalError)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
target,
|
|
184
|
+
...message ? { messageId: message.id } : {},
|
|
185
|
+
status: "error",
|
|
186
|
+
error: error instanceof Error ? error.message : String(error)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
async getMessage(messageId) {
|
|
192
|
+
const result2 = await this.request(`/v1/messages/${encodeURIComponent(messageId)}`);
|
|
193
|
+
return result2.message;
|
|
194
|
+
}
|
|
195
|
+
async acknowledge(messageId) {
|
|
196
|
+
const result2 = await this.request(
|
|
197
|
+
`/v1/messages/${encodeURIComponent(messageId)}/ack`,
|
|
198
|
+
{ method: "POST", body: "{}" }
|
|
199
|
+
);
|
|
200
|
+
return result2.message;
|
|
201
|
+
}
|
|
202
|
+
async reply(messageId, content) {
|
|
203
|
+
const result2 = await this.request(
|
|
204
|
+
`/v1/messages/${encodeURIComponent(messageId)}/reply`,
|
|
205
|
+
{ method: "POST", body: JSON.stringify({ content }) }
|
|
206
|
+
);
|
|
207
|
+
return result2.message;
|
|
208
|
+
}
|
|
209
|
+
async cancel(messageId) {
|
|
210
|
+
const result2 = await this.request(
|
|
211
|
+
`/v1/messages/${encodeURIComponent(messageId)}`,
|
|
212
|
+
{ method: "DELETE" }
|
|
213
|
+
);
|
|
214
|
+
return result2.message;
|
|
215
|
+
}
|
|
216
|
+
async listWorkflows() {
|
|
217
|
+
const result2 = await this.request("/v1/workflows");
|
|
218
|
+
return result2.runs;
|
|
219
|
+
}
|
|
220
|
+
async getWorkflow(runId) {
|
|
221
|
+
return await this.request(`/v1/workflows/${encodeURIComponent(runId)}`);
|
|
222
|
+
}
|
|
223
|
+
async checkpointWorkflow(runId, input) {
|
|
224
|
+
return await this.request(`/v1/workflows/${encodeURIComponent(runId)}/checkpoints`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
body: JSON.stringify(input)
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
async waitForWorkflowSignal(runId, input) {
|
|
230
|
+
return await this.request(`/v1/workflows/${encodeURIComponent(runId)}/waits`, {
|
|
231
|
+
method: "POST",
|
|
232
|
+
body: JSON.stringify(input)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async recordWorkflowEntry(runId, input) {
|
|
236
|
+
const result2 = await this.request(
|
|
237
|
+
`/v1/workflows/${encodeURIComponent(runId)}/journal`,
|
|
238
|
+
{ method: "POST", body: JSON.stringify(input) }
|
|
239
|
+
);
|
|
240
|
+
return result2.entry;
|
|
241
|
+
}
|
|
242
|
+
async improvementReport() {
|
|
243
|
+
return await this.request("/v1/improvements");
|
|
244
|
+
}
|
|
245
|
+
// ----- Context operating-system API (v0.5, issue #34) -----
|
|
246
|
+
async contextGet(input) {
|
|
247
|
+
return await this.request("/v1/context/get", { method: "POST", body: JSON.stringify(input) });
|
|
248
|
+
}
|
|
249
|
+
async contextRecall(input) {
|
|
250
|
+
return await this.request("/v1/context/recall", { method: "POST", body: JSON.stringify(input) });
|
|
251
|
+
}
|
|
252
|
+
async contextState(input) {
|
|
253
|
+
return await this.request("/v1/context/state", { method: "POST", body: JSON.stringify(input) });
|
|
254
|
+
}
|
|
255
|
+
async contextStatePropose(input) {
|
|
256
|
+
return await this.request("/v1/context/state/propose", { method: "POST", body: JSON.stringify(input) });
|
|
257
|
+
}
|
|
258
|
+
async contextStatePromote(input) {
|
|
259
|
+
return await this.request("/v1/context/state/promote", { method: "POST", body: JSON.stringify(input) });
|
|
260
|
+
}
|
|
261
|
+
async contextEpisode(input) {
|
|
262
|
+
return await this.request("/v1/context/episode", { method: "POST", body: JSON.stringify(input) });
|
|
263
|
+
}
|
|
264
|
+
async contextExplain(input) {
|
|
265
|
+
return await this.request("/v1/context/explain", { method: "POST", body: JSON.stringify(input) });
|
|
266
|
+
}
|
|
267
|
+
async contextWikiCompile(input) {
|
|
268
|
+
return await this.request("/v1/context/wiki/compile", { method: "POST", body: JSON.stringify(input) });
|
|
269
|
+
}
|
|
270
|
+
async awaitResponse(messageId, timeoutMs = 30 * 6e4, signal) {
|
|
271
|
+
const deadline = Date.now() + timeoutMs;
|
|
272
|
+
while (Date.now() < deadline) {
|
|
273
|
+
if (signal?.aborted) throw new MeshWaitError("aborted", messageId);
|
|
274
|
+
const message = await this.getMessage(messageId);
|
|
275
|
+
if (["replied", "cancelled", "expired", "error"].includes(message.status)) return message;
|
|
276
|
+
await new Promise((resolve3, reject) => {
|
|
277
|
+
const onAbort = () => {
|
|
278
|
+
signal?.removeEventListener("abort", onAbort);
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
reject(new MeshWaitError("aborted", messageId));
|
|
281
|
+
};
|
|
282
|
+
const timer = setTimeout(() => {
|
|
283
|
+
signal?.removeEventListener("abort", onAbort);
|
|
284
|
+
resolve3();
|
|
285
|
+
}, Math.min(500, Math.max(1, deadline - Date.now())));
|
|
286
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
287
|
+
if (signal?.aborted) onAbort();
|
|
288
|
+
timer.unref();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
throw new MeshWaitError("timed_out", messageId);
|
|
292
|
+
}
|
|
293
|
+
async heartbeat() {
|
|
294
|
+
if (this.stopped || !this.agent) return;
|
|
295
|
+
try {
|
|
296
|
+
await this.request(`/v1/agents/${encodeURIComponent(this.agent.id)}/heartbeat`, {
|
|
297
|
+
method: "POST",
|
|
298
|
+
body: "{}"
|
|
299
|
+
});
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error instanceof HubHttpError && error.statusCode === 401) void this.recoverRegistration();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async runEventLoop() {
|
|
305
|
+
while (!this.stopped && this.agent) {
|
|
306
|
+
this.eventsAbort = new AbortController();
|
|
307
|
+
try {
|
|
308
|
+
const response = await fetch(
|
|
309
|
+
`${this.options.serverUrl.replace(/\/$/, "")}/v1/events?agentId=${encodeURIComponent(this.agent.id)}`,
|
|
310
|
+
{
|
|
311
|
+
headers: this.headers(),
|
|
312
|
+
signal: this.eventsAbort.signal
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
if (response.status === 401) {
|
|
316
|
+
await response.body?.cancel();
|
|
317
|
+
await this.recoverRegistration();
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (!response.ok || !response.body) throw new Error(`event stream failed with HTTP ${response.status}`);
|
|
321
|
+
const decoder = new TextDecoder();
|
|
322
|
+
let buffer = "";
|
|
323
|
+
for await (const chunk of response.body) {
|
|
324
|
+
if (this.stopped) break;
|
|
325
|
+
buffer += decoder.decode(chunk, { stream: true }).replaceAll("\r\n", "\n");
|
|
326
|
+
let boundary;
|
|
327
|
+
while ((boundary = buffer.indexOf("\n\n")) >= 0) {
|
|
328
|
+
const frame = buffer.slice(0, boundary);
|
|
329
|
+
buffer = buffer.slice(boundary + 2);
|
|
330
|
+
const data = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
|
|
331
|
+
if (!data) continue;
|
|
332
|
+
const parsed = JSON.parse(data);
|
|
333
|
+
if ("type" in parsed) await this.onEvent?.(parsed);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (this.stopped || error instanceof Error && error.name === "AbortError") return;
|
|
338
|
+
}
|
|
339
|
+
if (!this.stopped) await new Promise((resolve3) => setTimeout(resolve3, this.options.reconnectMs));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
headers(includeIdentity = true) {
|
|
343
|
+
const headers = { "content-type": "application/json" };
|
|
344
|
+
if (this.options.authToken) headers.authorization = `Bearer ${this.options.authToken}`;
|
|
345
|
+
if (includeIdentity && this.agent && this.agentKey) {
|
|
346
|
+
headers["x-kxm-agent-id"] = this.agent.id;
|
|
347
|
+
headers["x-kxm-agent-key"] = this.agentKey;
|
|
348
|
+
}
|
|
349
|
+
return headers;
|
|
350
|
+
}
|
|
351
|
+
async register() {
|
|
352
|
+
if (this.registration) return this.registration;
|
|
353
|
+
this.registration = (async () => {
|
|
354
|
+
const registration = await this.request("/v1/agents/register", {
|
|
355
|
+
method: "POST",
|
|
356
|
+
body: JSON.stringify({
|
|
357
|
+
name: this.options.name,
|
|
358
|
+
purpose: this.options.purpose,
|
|
359
|
+
project: this.options.project,
|
|
360
|
+
model: this.options.model
|
|
361
|
+
})
|
|
362
|
+
}, false);
|
|
363
|
+
this.agent = registration.agent;
|
|
364
|
+
this.agentKey = registration.agentKey;
|
|
365
|
+
return registration.agent;
|
|
366
|
+
})();
|
|
367
|
+
try {
|
|
368
|
+
return await this.registration;
|
|
369
|
+
} finally {
|
|
370
|
+
this.registration = void 0;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async recoverRegistration() {
|
|
374
|
+
if (this.stopped) return;
|
|
375
|
+
this.agent = void 0;
|
|
376
|
+
this.agentKey = void 0;
|
|
377
|
+
await this.register();
|
|
378
|
+
}
|
|
379
|
+
async request(path, init = {}, includeIdentity = true) {
|
|
380
|
+
const requestTimeoutMs = this.options.requestTimeoutMs ?? 15e3;
|
|
381
|
+
const timeoutSignal = AbortSignal.timeout(requestTimeoutMs);
|
|
382
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
|
|
383
|
+
let response;
|
|
384
|
+
try {
|
|
385
|
+
response = await fetch(`${this.options.serverUrl.replace(/\/$/, "")}${path}`, {
|
|
386
|
+
...init,
|
|
387
|
+
signal,
|
|
388
|
+
headers: { ...this.headers(includeIdentity), ...init.headers ?? {} }
|
|
389
|
+
});
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (timeoutSignal.aborted) throw new Error(`request timed out after ${requestTimeoutMs}ms`);
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
const text = await response.text();
|
|
395
|
+
let body = {};
|
|
396
|
+
if (text) {
|
|
397
|
+
try {
|
|
398
|
+
body = JSON.parse(text);
|
|
399
|
+
} catch {
|
|
400
|
+
throw new Error(`hub returned invalid JSON with HTTP ${response.status}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!response.ok) {
|
|
404
|
+
const extras = {};
|
|
405
|
+
for (const key of ["operation", "nextAction", "assignedCoordinatorName"]) {
|
|
406
|
+
if (typeof body[key] === "string") extras[key] = body[key];
|
|
407
|
+
}
|
|
408
|
+
throw new HubHttpError(
|
|
409
|
+
response.status,
|
|
410
|
+
String(body.error ?? `HTTP ${response.status}`),
|
|
411
|
+
typeof body.code === "string" ? body.code : void 0,
|
|
412
|
+
response.headers.get("x-request-id") ?? void 0,
|
|
413
|
+
Object.keys(extras).length > 0 ? extras : void 0
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return body;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// plugins/kxm/src/commands.ts
|
|
421
|
+
function requiredString(value, name) {
|
|
422
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error(`${name} is required`);
|
|
423
|
+
return value.trim();
|
|
424
|
+
}
|
|
425
|
+
function optionalString(value) {
|
|
426
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
427
|
+
}
|
|
428
|
+
function asRecord(value) {
|
|
429
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
430
|
+
}
|
|
431
|
+
function optionalWorkflowContext(value) {
|
|
432
|
+
if (value === void 0) return void 0;
|
|
433
|
+
const context = asRecord(value);
|
|
434
|
+
if (!Number.isInteger(context.attempt) || context.attempt < 1 || context.attempt > 20) {
|
|
435
|
+
throw new Error("workflowContext.attempt must be an integer between 1 and 20");
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
runId: requiredString(context.runId, "workflowContext.runId"),
|
|
439
|
+
stageId: requiredString(context.stageId, "workflowContext.stageId"),
|
|
440
|
+
requirementKey: requiredString(context.requirementKey, "workflowContext.requirementKey"),
|
|
441
|
+
attempt: context.attempt
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function optionalEvidenceRefs(value) {
|
|
445
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
446
|
+
}
|
|
447
|
+
function isTerminalMessageError(error) {
|
|
448
|
+
return error instanceof HubHttpError && (error.statusCode === 409 || error.statusCode === 404 && error.code === "message_not_found");
|
|
449
|
+
}
|
|
450
|
+
function isTerminalMessage(message) {
|
|
451
|
+
return message.status === "replied" || message.status === "cancelled" || message.status === "expired" || message.status === "error";
|
|
452
|
+
}
|
|
453
|
+
async function reconcileInbox(client, inbox, notifiedInbox) {
|
|
454
|
+
await Promise.all(
|
|
455
|
+
[...inbox.keys()].map(async (messageId) => {
|
|
456
|
+
try {
|
|
457
|
+
const current = await client.getMessage(messageId);
|
|
458
|
+
if (isTerminalMessage(current)) {
|
|
459
|
+
inbox.delete(messageId);
|
|
460
|
+
notifiedInbox?.delete(messageId);
|
|
461
|
+
} else {
|
|
462
|
+
inbox.set(messageId, current);
|
|
463
|
+
}
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (isTerminalMessageError(error)) {
|
|
466
|
+
inbox.delete(messageId);
|
|
467
|
+
notifiedInbox?.delete(messageId);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
function resolveProject(client, projectArg) {
|
|
476
|
+
const proj = optionalString(projectArg) ?? client.agent?.project;
|
|
477
|
+
if (!proj) {
|
|
478
|
+
throw new Error('missing required parameter "project"');
|
|
479
|
+
}
|
|
480
|
+
return proj;
|
|
481
|
+
}
|
|
482
|
+
var AGENT_COMMANDS = [
|
|
483
|
+
{
|
|
484
|
+
name: "kxm_list",
|
|
485
|
+
group: "peer",
|
|
486
|
+
verb: "list",
|
|
487
|
+
label: "List hub peers",
|
|
488
|
+
description: "List online peer agents in this project's hub pool, including their names and purposes.",
|
|
489
|
+
parameters: {
|
|
490
|
+
type: "object",
|
|
491
|
+
properties: {},
|
|
492
|
+
additionalProperties: false
|
|
493
|
+
},
|
|
494
|
+
async execute(client) {
|
|
495
|
+
return { agents: await client.listAgents() };
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
name: "kxm_send",
|
|
500
|
+
group: "peer",
|
|
501
|
+
verb: "send",
|
|
502
|
+
label: "Send peer request",
|
|
503
|
+
description: "Send a focused request to a peer agent. Returns a message ID for kxm_get or kxm_await. For durable peer evidence, workflowContext is the hub-authorized provenance scope; correlation and idempotency are transport concerns and do not establish evidence provenance.",
|
|
504
|
+
parameters: {
|
|
505
|
+
type: "object",
|
|
506
|
+
properties: {
|
|
507
|
+
target: { type: "string", description: "Peer name or agent ID" },
|
|
508
|
+
content: { type: "string", description: "Focused request with the expected response or artifact" },
|
|
509
|
+
delivery: {
|
|
510
|
+
type: "string",
|
|
511
|
+
enum: ["steer", "followUp", "nextTurn"],
|
|
512
|
+
default: "followUp",
|
|
513
|
+
description: "followUp is the safe default; use steer only for active blockers"
|
|
514
|
+
},
|
|
515
|
+
correlationId: {
|
|
516
|
+
type: "string",
|
|
517
|
+
description: "Optional task grouping; the hub binds it to runId when workflowContext is supplied"
|
|
518
|
+
},
|
|
519
|
+
idempotencyKey: {
|
|
520
|
+
type: "string",
|
|
521
|
+
description: "Retry/deduplication key only; not a workflow security or evidence binding"
|
|
522
|
+
},
|
|
523
|
+
workflowContext: {
|
|
524
|
+
type: "object",
|
|
525
|
+
description: "Requested provenance scope; the hub authorizes and persists the canonical binding",
|
|
526
|
+
properties: {
|
|
527
|
+
runId: { type: "string", description: "Active durable workflow run ID" },
|
|
528
|
+
stageId: { type: "string", description: "Active workflow stage ID" },
|
|
529
|
+
requirementKey: { type: "string", description: "Required evidence identity this peer reply may satisfy" },
|
|
530
|
+
attempt: { type: "integer", minimum: 1, maximum: 20, description: "Current one-based stage attempt" }
|
|
531
|
+
},
|
|
532
|
+
required: ["runId", "stageId", "requirementKey", "attempt"],
|
|
533
|
+
additionalProperties: false
|
|
534
|
+
},
|
|
535
|
+
ttlMs: { type: "number", minimum: 1e3, maximum: 6048e5, description: "Message TTL in milliseconds" }
|
|
536
|
+
},
|
|
537
|
+
required: ["target", "content"],
|
|
538
|
+
additionalProperties: false
|
|
539
|
+
},
|
|
540
|
+
async execute(client, args) {
|
|
541
|
+
const delivery = optionalString(args.delivery);
|
|
542
|
+
const correlationId = optionalString(args.correlationId);
|
|
543
|
+
const idempotencyKey = optionalString(args.idempotencyKey);
|
|
544
|
+
const workflowContext = optionalWorkflowContext(args.workflowContext);
|
|
545
|
+
const message = await client.send({
|
|
546
|
+
target: requiredString(args.target, "target"),
|
|
547
|
+
content: requiredString(args.content, "content"),
|
|
548
|
+
...delivery ? { delivery } : {},
|
|
549
|
+
...correlationId ? { correlationId } : {},
|
|
550
|
+
...idempotencyKey ? { idempotencyKey } : {},
|
|
551
|
+
...workflowContext ? { workflowContext } : {},
|
|
552
|
+
...typeof args.ttlMs === "number" ? { ttlMs: args.ttlMs } : {}
|
|
553
|
+
});
|
|
554
|
+
return { messageId: message.id, status: message.status, target: message.toName };
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: "kxm_get",
|
|
559
|
+
group: "peer",
|
|
560
|
+
verb: "get",
|
|
561
|
+
label: "Get peer request",
|
|
562
|
+
description: "Check the status and optional reply for a previously sent request.",
|
|
563
|
+
parameters: {
|
|
564
|
+
type: "object",
|
|
565
|
+
properties: {
|
|
566
|
+
messageId: { type: "string", description: "Message ID of the sent request" }
|
|
567
|
+
},
|
|
568
|
+
required: ["messageId"],
|
|
569
|
+
additionalProperties: false
|
|
570
|
+
},
|
|
571
|
+
async execute(client, args) {
|
|
572
|
+
return await client.getMessage(requiredString(args.messageId, "messageId"));
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: "kxm_fanout",
|
|
577
|
+
group: "peer",
|
|
578
|
+
verb: "fanout",
|
|
579
|
+
label: "Fanout peer requests",
|
|
580
|
+
description: "Ask one through three peers independently and return replies for comparison and synthesis. A local timeout or request cancellation returns a pending response with a durable messageId for kxm_get or an exact retry.",
|
|
581
|
+
parameters: {
|
|
582
|
+
type: "object",
|
|
583
|
+
properties: {
|
|
584
|
+
targets: {
|
|
585
|
+
type: "array",
|
|
586
|
+
items: { type: "string" },
|
|
587
|
+
minItems: 1,
|
|
588
|
+
maxItems: 3,
|
|
589
|
+
description: "One through three target peer names or agent IDs"
|
|
590
|
+
},
|
|
591
|
+
content: { type: "string", description: "Task description sent to all targets" },
|
|
592
|
+
correlationId: {
|
|
593
|
+
type: "string",
|
|
594
|
+
description: "Optional task grouping; the hub binds it to runId when workflowContext is supplied"
|
|
595
|
+
},
|
|
596
|
+
idempotencyKeyPrefix: {
|
|
597
|
+
type: "string",
|
|
598
|
+
description: "Stable retry/deduplication prefix only; not a workflow security or evidence binding"
|
|
599
|
+
},
|
|
600
|
+
workflowContext: {
|
|
601
|
+
type: "object",
|
|
602
|
+
description: "Requested provenance scope shared by each request",
|
|
603
|
+
properties: {
|
|
604
|
+
runId: { type: "string", description: "Active durable workflow run ID" },
|
|
605
|
+
stageId: { type: "string", description: "Active workflow stage ID" },
|
|
606
|
+
requirementKey: { type: "string", description: "Required evidence identity these peer replies may satisfy" },
|
|
607
|
+
attempt: { type: "integer", minimum: 1, maximum: 20, description: "Current one-based stage attempt" }
|
|
608
|
+
},
|
|
609
|
+
required: ["runId", "stageId", "requirementKey", "attempt"],
|
|
610
|
+
additionalProperties: false
|
|
611
|
+
},
|
|
612
|
+
ttlMs: { type: "number", minimum: 1e3, maximum: 6048e5, description: "Message TTL in milliseconds" },
|
|
613
|
+
timeoutMs: { type: "number", minimum: 100, maximum: 18e5, description: "Client wait timeout in milliseconds" }
|
|
614
|
+
},
|
|
615
|
+
required: ["targets", "content"],
|
|
616
|
+
additionalProperties: false
|
|
617
|
+
},
|
|
618
|
+
async execute(client, args, context) {
|
|
619
|
+
const targets = Array.isArray(args.targets) ? args.targets.map((t) => requiredString(t, "target")) : [];
|
|
620
|
+
return {
|
|
621
|
+
responses: await client.fanout({
|
|
622
|
+
targets,
|
|
623
|
+
content: requiredString(args.content, "content"),
|
|
624
|
+
...optionalString(args.correlationId) ? { correlationId: optionalString(args.correlationId) } : {},
|
|
625
|
+
...optionalString(args.idempotencyKeyPrefix) ? { idempotencyKeyPrefix: optionalString(args.idempotencyKeyPrefix) } : {},
|
|
626
|
+
...optionalWorkflowContext(args.workflowContext) ? { workflowContext: optionalWorkflowContext(args.workflowContext) } : {},
|
|
627
|
+
...typeof args.ttlMs === "number" ? { ttlMs: args.ttlMs } : {},
|
|
628
|
+
...typeof args.timeoutMs === "number" ? { timeoutMs: args.timeoutMs } : {},
|
|
629
|
+
...context?.signal ? { signal: context.signal } : {}
|
|
630
|
+
})
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "kxm_await",
|
|
636
|
+
group: "peer",
|
|
637
|
+
verb: "await",
|
|
638
|
+
label: "Await peer response",
|
|
639
|
+
description: "Wait until a sent request receives a reply or reaches a terminal error. Capped at 60 seconds (60000ms); longer waits are workflow wait steps.",
|
|
640
|
+
parameters: {
|
|
641
|
+
type: "object",
|
|
642
|
+
properties: {
|
|
643
|
+
messageId: { type: "string", description: "Message ID of the sent request" },
|
|
644
|
+
timeoutMs: {
|
|
645
|
+
type: "number",
|
|
646
|
+
minimum: 100,
|
|
647
|
+
maximum: 6e4,
|
|
648
|
+
default: 6e4,
|
|
649
|
+
description: "Timeout in milliseconds (capped at 60 seconds)"
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
required: ["messageId"],
|
|
653
|
+
additionalProperties: false
|
|
654
|
+
},
|
|
655
|
+
async execute(client, args, context) {
|
|
656
|
+
const timeoutMs = Math.min(
|
|
657
|
+
typeof args.timeoutMs === "number" ? args.timeoutMs : 6e4,
|
|
658
|
+
6e4
|
|
659
|
+
);
|
|
660
|
+
return await client.awaitResponse(
|
|
661
|
+
requiredString(args.messageId, "messageId"),
|
|
662
|
+
timeoutMs,
|
|
663
|
+
context?.signal
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
name: "kxm_cancel",
|
|
669
|
+
group: "peer",
|
|
670
|
+
verb: "cancel",
|
|
671
|
+
label: "Cancel peer request",
|
|
672
|
+
description: "Cancel a queued or delivered request sent by this agent.",
|
|
673
|
+
parameters: {
|
|
674
|
+
type: "object",
|
|
675
|
+
properties: {
|
|
676
|
+
messageId: { type: "string", description: "Message ID of the request to cancel" }
|
|
677
|
+
},
|
|
678
|
+
required: ["messageId"],
|
|
679
|
+
additionalProperties: false
|
|
680
|
+
},
|
|
681
|
+
async execute(client, args) {
|
|
682
|
+
return await client.cancel(requiredString(args.messageId, "messageId"));
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
name: "kxm_inbox",
|
|
687
|
+
group: "peer",
|
|
688
|
+
verb: "inbox",
|
|
689
|
+
label: "List inbound requests",
|
|
690
|
+
description: "List inbound peer requests awaiting a reply.",
|
|
691
|
+
parameters: {
|
|
692
|
+
type: "object",
|
|
693
|
+
properties: {},
|
|
694
|
+
additionalProperties: false
|
|
695
|
+
},
|
|
696
|
+
async execute(client, _args, context) {
|
|
697
|
+
if (context?.inbox) {
|
|
698
|
+
await reconcileInbox(client, context.inbox, context.notifiedInbox);
|
|
699
|
+
return { messages: [...context.inbox.values()] };
|
|
700
|
+
}
|
|
701
|
+
return { messages: [] };
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
name: "kxm_reply",
|
|
706
|
+
group: "peer",
|
|
707
|
+
verb: "reply",
|
|
708
|
+
label: "Reply to peer request",
|
|
709
|
+
description: "Reply to an inbound peer request using its message ID.",
|
|
710
|
+
parameters: {
|
|
711
|
+
type: "object",
|
|
712
|
+
properties: {
|
|
713
|
+
messageId: { type: "string", description: "Message ID of the inbound request" },
|
|
714
|
+
content: { type: "string", description: "Final response with evidence and remaining risks" }
|
|
715
|
+
},
|
|
716
|
+
required: ["messageId", "content"],
|
|
717
|
+
additionalProperties: false
|
|
718
|
+
},
|
|
719
|
+
async execute(client, args, context) {
|
|
720
|
+
const messageId = requiredString(args.messageId, "messageId");
|
|
721
|
+
try {
|
|
722
|
+
const message = await client.reply(messageId, requiredString(args.content, "content"));
|
|
723
|
+
if (context?.inbox) {
|
|
724
|
+
context.inbox.delete(messageId);
|
|
725
|
+
context.notifiedInbox?.delete(messageId);
|
|
726
|
+
}
|
|
727
|
+
return { messageId, status: message.status, recipient: message.fromName };
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (context?.inbox && isTerminalMessageError(error)) {
|
|
730
|
+
context.inbox.delete(messageId);
|
|
731
|
+
context.notifiedInbox?.delete(messageId);
|
|
732
|
+
}
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
name: "kxm_workflow_list",
|
|
739
|
+
group: "workflow",
|
|
740
|
+
verb: "runs",
|
|
741
|
+
label: "List workflow runs",
|
|
742
|
+
description: "List durable webhook workflows assigned to this agent.",
|
|
743
|
+
parameters: {
|
|
744
|
+
type: "object",
|
|
745
|
+
properties: {},
|
|
746
|
+
additionalProperties: false
|
|
747
|
+
},
|
|
748
|
+
async execute(client) {
|
|
749
|
+
return { runs: await client.listWorkflows() };
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
name: "kxm_workflow_get",
|
|
754
|
+
group: "workflow",
|
|
755
|
+
verb: "run",
|
|
756
|
+
label: "Get workflow run",
|
|
757
|
+
description: "Get a workflow's stages and journal of plans, decisions, contradictions, errors, and lessons.",
|
|
758
|
+
parameters: {
|
|
759
|
+
type: "object",
|
|
760
|
+
properties: {
|
|
761
|
+
runId: { type: "string", description: "Workflow run ID" }
|
|
762
|
+
},
|
|
763
|
+
required: ["runId"],
|
|
764
|
+
additionalProperties: false
|
|
765
|
+
},
|
|
766
|
+
async execute(client, args) {
|
|
767
|
+
return await client.getWorkflow(requiredString(args.runId, "runId"));
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
name: "kxm_workflow_checkpoint",
|
|
772
|
+
group: "workflow",
|
|
773
|
+
verb: "checkpoint",
|
|
774
|
+
label: "Checkpoint workflow stage",
|
|
775
|
+
description: "Record a stage result with evidence keyed by the stage's required evidence identities. Cite peer-reply requirements through evidenceRefs so the hub can verify provenance and quorum; caller-authored evidence strings cannot satisfy those policies. Warnings and failures require another attempt until passed or exhausted.",
|
|
776
|
+
parameters: {
|
|
777
|
+
type: "object",
|
|
778
|
+
properties: {
|
|
779
|
+
runId: { type: "string", description: "Active durable workflow run ID" },
|
|
780
|
+
stageId: { type: "string", description: "Active stage ID" },
|
|
781
|
+
status: { type: "string", enum: ["passed", "warning", "failed"], description: "Stage outcome" },
|
|
782
|
+
summary: { type: "string", description: "Summary of changes, verification, and remaining risks" },
|
|
783
|
+
evidence: {
|
|
784
|
+
type: "object",
|
|
785
|
+
additionalProperties: { type: "string" },
|
|
786
|
+
maxProperties: 64,
|
|
787
|
+
description: "Key-value evidence mapping required keys to proof strings"
|
|
788
|
+
},
|
|
789
|
+
evidenceRefs: {
|
|
790
|
+
type: "object",
|
|
791
|
+
description: "Peer evidence references keyed by required evidence identity",
|
|
792
|
+
patternProperties: {
|
|
793
|
+
"^(.*)$": {
|
|
794
|
+
type: "object",
|
|
795
|
+
properties: {
|
|
796
|
+
messageIds: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 16 }
|
|
797
|
+
},
|
|
798
|
+
required: ["messageIds"],
|
|
799
|
+
additionalProperties: false
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
additionalProperties: {
|
|
803
|
+
type: "object",
|
|
804
|
+
properties: {
|
|
805
|
+
messageIds: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 16 }
|
|
806
|
+
},
|
|
807
|
+
required: ["messageIds"],
|
|
808
|
+
additionalProperties: false
|
|
809
|
+
},
|
|
810
|
+
maxProperties: 32
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
required: ["runId", "stageId", "status", "summary"],
|
|
814
|
+
additionalProperties: false
|
|
815
|
+
},
|
|
816
|
+
async execute(client, args) {
|
|
817
|
+
return await client.checkpointWorkflow(requiredString(args.runId, "runId"), {
|
|
818
|
+
stageId: requiredString(args.stageId, "stageId"),
|
|
819
|
+
status: requiredString(args.status, "status"),
|
|
820
|
+
summary: requiredString(args.summary, "summary"),
|
|
821
|
+
...args.evidence && typeof args.evidence === "object" && !Array.isArray(args.evidence) ? { evidence: args.evidence } : {},
|
|
822
|
+
...optionalEvidenceRefs(args.evidenceRefs) ? { evidenceRefs: optionalEvidenceRefs(args.evidenceRefs) } : {}
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
name: "kxm_workflow_record",
|
|
828
|
+
group: "workflow",
|
|
829
|
+
verb: "record",
|
|
830
|
+
label: "Record workflow journal entry",
|
|
831
|
+
description: "Record a plan, decision, contradiction, error, or lesson for continuous improvement.",
|
|
832
|
+
parameters: {
|
|
833
|
+
type: "object",
|
|
834
|
+
properties: {
|
|
835
|
+
runId: { type: "string", description: "Active durable workflow run ID" },
|
|
836
|
+
category: {
|
|
837
|
+
type: "string",
|
|
838
|
+
enum: ["plan", "decision", "contradiction", "error", "lesson"],
|
|
839
|
+
description: "Category of journal entry"
|
|
840
|
+
},
|
|
841
|
+
area: {
|
|
842
|
+
type: "string",
|
|
843
|
+
enum: ["harness", "gates", "implementation", "workflow", "documentation", "security", "other"],
|
|
844
|
+
description: "System area"
|
|
845
|
+
},
|
|
846
|
+
severity: {
|
|
847
|
+
type: "string",
|
|
848
|
+
enum: ["info", "warning", "error"],
|
|
849
|
+
default: "info",
|
|
850
|
+
description: "Severity level"
|
|
851
|
+
},
|
|
852
|
+
summary: { type: "string", description: "Concise description of the observation or decision" },
|
|
853
|
+
details: { type: "string", description: "Extended details, context, and reasoning" },
|
|
854
|
+
evidence: {
|
|
855
|
+
type: "array",
|
|
856
|
+
items: { type: "string" },
|
|
857
|
+
maxItems: 32,
|
|
858
|
+
description: "Durable evidence strings or URIs"
|
|
859
|
+
},
|
|
860
|
+
relatedEntryIds: {
|
|
861
|
+
type: "array",
|
|
862
|
+
items: { type: "string" },
|
|
863
|
+
maxItems: 16,
|
|
864
|
+
description: "Related previous journal entry IDs"
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
required: ["runId", "category", "area", "summary"],
|
|
868
|
+
additionalProperties: false
|
|
869
|
+
},
|
|
870
|
+
async execute(client, args) {
|
|
871
|
+
return await client.recordWorkflowEntry(requiredString(args.runId, "runId"), {
|
|
872
|
+
category: requiredString(args.category, "category"),
|
|
873
|
+
area: requiredString(args.area, "area"),
|
|
874
|
+
...optionalString(args.severity) ? { severity: optionalString(args.severity) } : {},
|
|
875
|
+
summary: requiredString(args.summary, "summary"),
|
|
876
|
+
...optionalString(args.details) ? { details: optionalString(args.details) } : {},
|
|
877
|
+
...Array.isArray(args.evidence) ? { evidence: args.evidence } : {},
|
|
878
|
+
...Array.isArray(args.relatedEntryIds) ? { relatedEntryIds: args.relatedEntryIds } : {}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
name: "kxm_workflow_wait",
|
|
884
|
+
group: "workflow",
|
|
885
|
+
verb: "wait",
|
|
886
|
+
label: "Wait for workflow signal",
|
|
887
|
+
description: "Pause the active stage until a signed external callback checkpoints it and resumes the coordinator. Cite peer-reply requirements through evidenceRefs so the hub can verify provenance and quorum; verified evidence is accumulated with callback evidence.",
|
|
888
|
+
parameters: {
|
|
889
|
+
type: "object",
|
|
890
|
+
properties: {
|
|
891
|
+
runId: { type: "string", description: "Active durable workflow run ID" },
|
|
892
|
+
stageId: { type: "string", description: "Active stage ID" },
|
|
893
|
+
signalKey: { type: "string", description: "Stable callback key, such as github-pr-42-checks" },
|
|
894
|
+
summary: { type: "string", description: "What is running externally and what result is expected" },
|
|
895
|
+
evidence: {
|
|
896
|
+
type: "object",
|
|
897
|
+
additionalProperties: { type: "string" },
|
|
898
|
+
maxProperties: 64,
|
|
899
|
+
description: "Evidence gathered before the wait"
|
|
900
|
+
},
|
|
901
|
+
evidenceRefs: {
|
|
902
|
+
type: "object",
|
|
903
|
+
description: "Peer evidence references verified before waiting",
|
|
904
|
+
patternProperties: {
|
|
905
|
+
"^(.*)$": {
|
|
906
|
+
type: "object",
|
|
907
|
+
properties: {
|
|
908
|
+
messageIds: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 16 }
|
|
909
|
+
},
|
|
910
|
+
required: ["messageIds"],
|
|
911
|
+
additionalProperties: false
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
additionalProperties: {
|
|
915
|
+
type: "object",
|
|
916
|
+
properties: {
|
|
917
|
+
messageIds: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 16 }
|
|
918
|
+
},
|
|
919
|
+
required: ["messageIds"],
|
|
920
|
+
additionalProperties: false
|
|
921
|
+
},
|
|
922
|
+
maxProperties: 32
|
|
923
|
+
},
|
|
924
|
+
timeoutMs: {
|
|
925
|
+
type: "number",
|
|
926
|
+
minimum: 1e3,
|
|
927
|
+
maximum: 2592e6,
|
|
928
|
+
description: "Maximum wait duration in milliseconds"
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
required: ["runId", "stageId", "signalKey", "summary"],
|
|
932
|
+
additionalProperties: false
|
|
933
|
+
},
|
|
934
|
+
async execute(client, args) {
|
|
935
|
+
return await client.waitForWorkflowSignal(requiredString(args.runId, "runId"), {
|
|
936
|
+
stageId: requiredString(args.stageId, "stageId"),
|
|
937
|
+
signalKey: requiredString(args.signalKey, "signalKey"),
|
|
938
|
+
summary: requiredString(args.summary, "summary"),
|
|
939
|
+
...args.evidence && typeof args.evidence === "object" && !Array.isArray(args.evidence) ? { evidence: args.evidence } : {},
|
|
940
|
+
...optionalEvidenceRefs(args.evidenceRefs) ? { evidenceRefs: optionalEvidenceRefs(args.evidenceRefs) } : {},
|
|
941
|
+
...typeof args.timeoutMs === "number" ? { timeoutMs: args.timeoutMs } : {}
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
name: "kxm_improvement_report",
|
|
947
|
+
group: "workflow",
|
|
948
|
+
verb: "improve-report",
|
|
949
|
+
label: "Summarize improvement report",
|
|
950
|
+
description: "Summarize workflow errors, contradictions, and lessons by improvement area.",
|
|
951
|
+
parameters: {
|
|
952
|
+
type: "object",
|
|
953
|
+
properties: {},
|
|
954
|
+
additionalProperties: false
|
|
955
|
+
},
|
|
956
|
+
async execute(client) {
|
|
957
|
+
return await client.improvementReport();
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
name: "kxm_context",
|
|
962
|
+
group: "context",
|
|
963
|
+
verb: "get",
|
|
964
|
+
label: "Get KXM context packet",
|
|
965
|
+
description: "Normal entry point for KXM context. Assembles a token-budgeted role-aware context packet from durable journal evidence, temporal state, knowledge, episodes, and skills. Superseded and rejected records are excluded. Use KXM context tools instead of provider-specific memory APIs.",
|
|
966
|
+
parameters: {
|
|
967
|
+
type: "object",
|
|
968
|
+
properties: {
|
|
969
|
+
project: { type: "string", description: "Project scope (must be the client's project)" },
|
|
970
|
+
role: {
|
|
971
|
+
type: "string",
|
|
972
|
+
description: "Requesting role: repro, planner, critic, implementer, verifier, or custom"
|
|
973
|
+
},
|
|
974
|
+
task: { type: "string", description: "What the role is trying to accomplish" },
|
|
975
|
+
workflowRunId: { type: "string", description: "Workflow run scope" },
|
|
976
|
+
stageId: { type: "string", description: "Workflow stage scope" },
|
|
977
|
+
budgetTokens: { type: "integer", description: "Token budget; defaults to the role policy" },
|
|
978
|
+
includeKinds: {
|
|
979
|
+
type: "array",
|
|
980
|
+
items: { type: "string", enum: ["evidence", "state", "episode", "knowledge", "skill"] },
|
|
981
|
+
description: "Restrict packet to these item kinds"
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
required: ["role", "task"],
|
|
985
|
+
additionalProperties: false
|
|
986
|
+
},
|
|
987
|
+
async execute(client, args) {
|
|
988
|
+
return await client.contextGet({
|
|
989
|
+
project: resolveProject(client, args.project),
|
|
990
|
+
role: requiredString(args.role, "role"),
|
|
991
|
+
task: requiredString(args.task, "task"),
|
|
992
|
+
...optionalString(args.workflowRunId) ? { workflowRunId: optionalString(args.workflowRunId) } : {},
|
|
993
|
+
...optionalString(args.stageId) ? { stageId: optionalString(args.stageId) } : {},
|
|
994
|
+
...typeof args.budgetTokens === "number" ? { budgetTokens: args.budgetTokens } : {},
|
|
995
|
+
...Array.isArray(args.includeKinds) ? { includeKinds: args.includeKinds } : {}
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
name: "kxm_recall",
|
|
1001
|
+
group: "context",
|
|
1002
|
+
verb: "recall",
|
|
1003
|
+
label: "Recall context metadata",
|
|
1004
|
+
description: "Search durable context records for a project by query; returns bounded metadata only.",
|
|
1005
|
+
parameters: {
|
|
1006
|
+
type: "object",
|
|
1007
|
+
properties: {
|
|
1008
|
+
project: { type: "string", description: "Project identifier" },
|
|
1009
|
+
query: { type: "string", description: "Query string" },
|
|
1010
|
+
kinds: { type: "array", items: { type: "string" }, description: "Kinds filter" },
|
|
1011
|
+
limit: { type: "integer", minimum: 1, maximum: 100, description: "Maximum results" }
|
|
1012
|
+
},
|
|
1013
|
+
additionalProperties: false
|
|
1014
|
+
},
|
|
1015
|
+
async execute(client, args) {
|
|
1016
|
+
return await client.contextRecall({
|
|
1017
|
+
project: resolveProject(client, args.project),
|
|
1018
|
+
...optionalString(args.query) ? { query: optionalString(args.query) } : {},
|
|
1019
|
+
...Array.isArray(args.kinds) ? { kinds: args.kinds } : {},
|
|
1020
|
+
...typeof args.limit === "number" ? { limit: args.limit } : {}
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
name: "kxm_state",
|
|
1026
|
+
group: "context",
|
|
1027
|
+
verb: "state",
|
|
1028
|
+
label: "Get temporal state",
|
|
1029
|
+
description: "Current value for one temporal state key, optionally as of a historical timestamp.",
|
|
1030
|
+
parameters: {
|
|
1031
|
+
type: "object",
|
|
1032
|
+
properties: {
|
|
1033
|
+
project: { type: "string", description: "Project identifier" },
|
|
1034
|
+
key: { type: "string", description: "State key" },
|
|
1035
|
+
asOf: { type: "string", description: "ISO-8601 timestamp for historical queries" }
|
|
1036
|
+
},
|
|
1037
|
+
required: ["key"],
|
|
1038
|
+
additionalProperties: false
|
|
1039
|
+
},
|
|
1040
|
+
async execute(client, args) {
|
|
1041
|
+
return await client.contextState({
|
|
1042
|
+
project: resolveProject(client, args.project),
|
|
1043
|
+
key: requiredString(args.key, "key"),
|
|
1044
|
+
...optionalString(args.asOf) ? { asOf: optionalString(args.asOf) } : {}
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
name: "kxm_episode",
|
|
1050
|
+
group: "context",
|
|
1051
|
+
verb: "episode",
|
|
1052
|
+
label: "Get workflow episodes",
|
|
1053
|
+
description: "Episodic learning from workflow journals: errors, lessons, observations, experiments for a project.",
|
|
1054
|
+
parameters: {
|
|
1055
|
+
type: "object",
|
|
1056
|
+
properties: {
|
|
1057
|
+
project: { type: "string", description: "Project identifier" },
|
|
1058
|
+
workflowRunId: { type: "string", description: "Optional workflow run scope" }
|
|
1059
|
+
},
|
|
1060
|
+
additionalProperties: false
|
|
1061
|
+
},
|
|
1062
|
+
async execute(client, args) {
|
|
1063
|
+
return await client.contextEpisode({
|
|
1064
|
+
project: resolveProject(client, args.project),
|
|
1065
|
+
...optionalString(args.workflowRunId) ? { workflowRunId: optionalString(args.workflowRunId) } : {}
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
name: "kxm_promote",
|
|
1071
|
+
group: "context",
|
|
1072
|
+
verb: "promote",
|
|
1073
|
+
label: "Propose state promotion",
|
|
1074
|
+
description: "Propose a change to one authoritative state key. Promotion requires durable evidence.",
|
|
1075
|
+
parameters: {
|
|
1076
|
+
type: "object",
|
|
1077
|
+
properties: {
|
|
1078
|
+
project: { type: "string", description: "Project identifier" },
|
|
1079
|
+
key: { type: "string", description: "State key to promote" },
|
|
1080
|
+
summary: { type: "string", description: "Promotion summary" },
|
|
1081
|
+
authority: {
|
|
1082
|
+
type: "string",
|
|
1083
|
+
enum: ["policy", "instruction", "evidence", "hypothesis"],
|
|
1084
|
+
description: "Authority class"
|
|
1085
|
+
},
|
|
1086
|
+
confidence: {
|
|
1087
|
+
type: "string",
|
|
1088
|
+
enum: ["verified", "probable", "uncertain"],
|
|
1089
|
+
description: "Confidence level"
|
|
1090
|
+
},
|
|
1091
|
+
evidenceRefs: {
|
|
1092
|
+
type: "array",
|
|
1093
|
+
items: { type: "string" },
|
|
1094
|
+
minItems: 1,
|
|
1095
|
+
maxItems: 32,
|
|
1096
|
+
description: "Evidence item references backing the promotion"
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
required: ["key", "summary", "authority", "confidence", "evidenceRefs"],
|
|
1100
|
+
additionalProperties: false
|
|
1101
|
+
},
|
|
1102
|
+
async execute(client, args) {
|
|
1103
|
+
return await client.contextStatePropose({
|
|
1104
|
+
project: resolveProject(client, args.project),
|
|
1105
|
+
key: requiredString(args.key, "key"),
|
|
1106
|
+
summary: requiredString(args.summary, "summary"),
|
|
1107
|
+
authority: requiredString(args.authority, "authority"),
|
|
1108
|
+
confidence: requiredString(args.confidence, "confidence"),
|
|
1109
|
+
evidenceRefs: Array.isArray(args.evidenceRefs) ? args.evidenceRefs : []
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
];
|
|
1114
|
+
var AGENT_COMMANDS_MAP = new Map(
|
|
1115
|
+
AGENT_COMMANDS.map((cmd) => [cmd.name, cmd])
|
|
1116
|
+
);
|
|
1117
|
+
function parseAttemptToken(token) {
|
|
1118
|
+
try {
|
|
1119
|
+
const raw = Buffer.from(token.trim(), "base64url").toString("utf8");
|
|
1120
|
+
const parsed = JSON.parse(raw);
|
|
1121
|
+
if (parsed && typeof parsed === "object" && parsed.schema === "kxm.attempt-token.v1" && typeof parsed.runId === "string") {
|
|
1122
|
+
return parsed;
|
|
1123
|
+
}
|
|
1124
|
+
} catch {
|
|
1125
|
+
return void 0;
|
|
1126
|
+
}
|
|
1127
|
+
return void 0;
|
|
1128
|
+
}
|
|
1129
|
+
function parseSessionToken(token) {
|
|
1130
|
+
try {
|
|
1131
|
+
const raw = Buffer.from(token.trim(), "base64url").toString("utf8");
|
|
1132
|
+
const parsed = JSON.parse(raw);
|
|
1133
|
+
if (parsed && typeof parsed === "object" && parsed.schema === "kxm.session-token.v1" && typeof parsed.sessionId === "string") {
|
|
1134
|
+
return parsed;
|
|
1135
|
+
}
|
|
1136
|
+
} catch {
|
|
1137
|
+
return void 0;
|
|
1138
|
+
}
|
|
1139
|
+
return void 0;
|
|
1140
|
+
}
|
|
1141
|
+
function matchToolPattern(pattern, toolName) {
|
|
1142
|
+
if (pattern === "*" || pattern === toolName) return true;
|
|
1143
|
+
if (pattern.endsWith("*")) {
|
|
1144
|
+
const prefix = pattern.slice(0, -1);
|
|
1145
|
+
return toolName.startsWith(prefix);
|
|
1146
|
+
}
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
function isToolAllowed(commandName, policy) {
|
|
1150
|
+
if (!policy) return true;
|
|
1151
|
+
const canonical = commandName.startsWith("kxm_") ? commandName : `kxm_${commandName}`;
|
|
1152
|
+
const bare = commandName.replace(/^kxm_/, "");
|
|
1153
|
+
const denyList = policy.deny ?? policy.deniedTools;
|
|
1154
|
+
if (Array.isArray(denyList)) {
|
|
1155
|
+
for (const d of denyList) {
|
|
1156
|
+
if (d === canonical || d === commandName || d === bare || matchToolPattern(d, canonical)) {
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
const allowList = policy.allow ?? policy.allowedTools;
|
|
1162
|
+
if (Array.isArray(allowList) && allowList.length > 0) {
|
|
1163
|
+
const matched = allowList.some(
|
|
1164
|
+
(a) => a === canonical || a === commandName || a === bare || a === "*" || matchToolPattern(a, canonical)
|
|
1165
|
+
);
|
|
1166
|
+
if (!matched) return false;
|
|
1167
|
+
}
|
|
1168
|
+
if (policy.preset === "read-only") {
|
|
1169
|
+
const mutating = [
|
|
1170
|
+
"kxm_send",
|
|
1171
|
+
"kxm_reply",
|
|
1172
|
+
"kxm_cancel",
|
|
1173
|
+
"kxm_fanout",
|
|
1174
|
+
"kxm_workflow_checkpoint",
|
|
1175
|
+
"kxm_workflow_record",
|
|
1176
|
+
"kxm_workflow_wait",
|
|
1177
|
+
"kxm_promote"
|
|
1178
|
+
];
|
|
1179
|
+
if (mutating.includes(canonical)) return false;
|
|
1180
|
+
}
|
|
1181
|
+
return true;
|
|
1182
|
+
}
|
|
1183
|
+
function enforceToolPolicy(commandName, env = process.env) {
|
|
1184
|
+
const attemptTokenRaw = env.KXM_ATTEMPT_TOKEN?.trim();
|
|
1185
|
+
if (attemptTokenRaw) {
|
|
1186
|
+
const attempt = parseAttemptToken(attemptTokenRaw);
|
|
1187
|
+
if (!attempt) {
|
|
1188
|
+
return { allowed: false, error: "attempt_token_invalid", detail: "KXM_ATTEMPT_TOKEN is malformed" };
|
|
1189
|
+
}
|
|
1190
|
+
if (!isToolAllowed(commandName, attempt.toolPolicy)) {
|
|
1191
|
+
return {
|
|
1192
|
+
allowed: false,
|
|
1193
|
+
error: "tool_policy_denied",
|
|
1194
|
+
detail: `command ${commandName} is denied by attempt tool policy`
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
return { allowed: true };
|
|
1198
|
+
}
|
|
1199
|
+
const sessionTokenRaw = env.KXM_SESSION_TOKEN?.trim();
|
|
1200
|
+
if (sessionTokenRaw) {
|
|
1201
|
+
const session = parseSessionToken(sessionTokenRaw);
|
|
1202
|
+
if (!session) {
|
|
1203
|
+
return { allowed: false, error: "session_token_invalid", detail: "KXM_SESSION_TOKEN is malformed" };
|
|
1204
|
+
}
|
|
1205
|
+
if (!isToolAllowed(commandName, session.toolPolicy)) {
|
|
1206
|
+
return {
|
|
1207
|
+
allowed: false,
|
|
1208
|
+
error: "tool_policy_denied",
|
|
1209
|
+
detail: `command ${commandName} is denied by session tool policy`
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
return { allowed: true };
|
|
1213
|
+
}
|
|
1214
|
+
return { allowed: true };
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// plugins/kxm/src/nous-pi.ts
|
|
1218
|
+
import { readFile } from "node:fs/promises";
|
|
1219
|
+
|
|
1220
|
+
// plugins/kxm/src/nous-provider.ts
|
|
1221
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1222
|
+
|
|
1223
|
+
// plugins/kxm/src/redact.ts
|
|
1224
|
+
var SECRET_PATTERNS = [
|
|
1225
|
+
/\bsk-[A-Za-z0-9_-]{8,}\b/g,
|
|
1226
|
+
/\bghp_[A-Za-z0-9_]{20,}\b/g,
|
|
1227
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
1228
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/gi,
|
|
1229
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]+\b/gi,
|
|
1230
|
+
/\bKXM_[A-Z0-9_]*(TOKEN|SECRET|KEY)[A-Z0-9_]*=\S+/gi,
|
|
1231
|
+
/\b(GITHUB_TOKEN|GH_TOKEN|KXM_AUTH_TOKEN|KXM_WORKFLOW_SIGNAL_SECRET)=\S+/gi,
|
|
1232
|
+
/\b[A-Fa-f0-9]{64}\b/g
|
|
1233
|
+
];
|
|
1234
|
+
function redactSecrets(value) {
|
|
1235
|
+
let result2 = value;
|
|
1236
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
1237
|
+
result2 = result2.replace(pattern, "[redacted]");
|
|
1238
|
+
}
|
|
1239
|
+
return result2;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// plugins/kxm/src/nous-provider.ts
|
|
1243
|
+
var NOUS_DIRECT_ID = "nous";
|
|
1244
|
+
var NOUS_PROXY_ID = "nous-proxy";
|
|
1245
|
+
var NOUS_DIRECT_BASE_URL = "https://inference-api.nousresearch.com/v1";
|
|
1246
|
+
var NOUS_PROXY_DEFAULT_BASE_URL = "http://127.0.0.1:8645/v1";
|
|
1247
|
+
var NOUS_PROXY_PLACEHOLDER_KEY = "kxm-nous-proxy";
|
|
1248
|
+
var NOUS_CATALOG_SCHEMA = "kxm.nous-catalog.v1";
|
|
1249
|
+
var NOUS_PRICE_UNITS = "usd_per_million_tokens";
|
|
1250
|
+
var DEFAULT_DISCOVERY_TIMEOUT_MS = 5e3;
|
|
1251
|
+
var NOUS_CATALOG_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1252
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
1253
|
+
var PROVIDER_TOKENS = /* @__PURE__ */ new Set(["direct", "proxy"]);
|
|
1254
|
+
var PER_TOKEN_TO_USD_PER_M = 1e6;
|
|
1255
|
+
var PI_INPUT_ORDER = ["text", "image"];
|
|
1256
|
+
function parseNousEnv(env = process.env) {
|
|
1257
|
+
const raw = env.KXM_NOUS_PROVIDERS?.trim() ?? "";
|
|
1258
|
+
const timeoutMs = parseTimeout(env.KXM_NOUS_DISCOVERY_TIMEOUT_MS);
|
|
1259
|
+
if (!raw) {
|
|
1260
|
+
return { status: "unset", providers: [], timeoutMs };
|
|
1261
|
+
}
|
|
1262
|
+
const tokens = raw.split(",").map((part) => part.trim()).filter(Boolean);
|
|
1263
|
+
const unknownTokens = [...new Set(tokens.filter((token) => !PROVIDER_TOKENS.has(token)))];
|
|
1264
|
+
if (unknownTokens.length > 0) {
|
|
1265
|
+
return {
|
|
1266
|
+
status: "invalid",
|
|
1267
|
+
providers: [],
|
|
1268
|
+
unknownTokens,
|
|
1269
|
+
timeoutMs,
|
|
1270
|
+
error: `unknown KXM_NOUS_PROVIDERS token(s): ${unknownTokens.join(", ")}; expected direct and/or proxy`
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1274
|
+
return {
|
|
1275
|
+
status: "invalid",
|
|
1276
|
+
providers: [],
|
|
1277
|
+
unknownTokens: [],
|
|
1278
|
+
timeoutMs: DEFAULT_DISCOVERY_TIMEOUT_MS,
|
|
1279
|
+
error: "KXM_NOUS_DISCOVERY_TIMEOUT_MS must be a positive finite number of milliseconds"
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
const providers = [...new Set(tokens)];
|
|
1283
|
+
const catalogFile = env.KXM_NOUS_CATALOG_FILE?.trim();
|
|
1284
|
+
const proxyOverride = env.KXM_NOUS_PROXY_URL?.trim();
|
|
1285
|
+
return {
|
|
1286
|
+
status: "ready",
|
|
1287
|
+
providers,
|
|
1288
|
+
timeoutMs,
|
|
1289
|
+
proxyUrl: proxyOverride && proxyOverride.length > 0 ? proxyOverride : NOUS_PROXY_DEFAULT_BASE_URL,
|
|
1290
|
+
...catalogFile ? { catalogFile } : {}
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
function parseTimeout(raw) {
|
|
1294
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_DISCOVERY_TIMEOUT_MS;
|
|
1295
|
+
const value = Number(raw);
|
|
1296
|
+
return value;
|
|
1297
|
+
}
|
|
1298
|
+
function isLoopbackUrl(value) {
|
|
1299
|
+
let parsed;
|
|
1300
|
+
try {
|
|
1301
|
+
parsed = new URL(value);
|
|
1302
|
+
} catch {
|
|
1303
|
+
return false;
|
|
1304
|
+
}
|
|
1305
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
1306
|
+
if (parsed.username || parsed.password) return false;
|
|
1307
|
+
const host = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
1308
|
+
return LOOPBACK_HOSTS.has(host);
|
|
1309
|
+
}
|
|
1310
|
+
function modelsUrl(baseUrl) {
|
|
1311
|
+
return `${baseUrl.replace(/\/+$/, "")}/models`;
|
|
1312
|
+
}
|
|
1313
|
+
function sanitizeNousText(value) {
|
|
1314
|
+
const stripped = value.replace(/\bBearer\s+\S+/gi, "Bearer [redacted]").replace(/\b(NOUS_API_KEY|OPENROUTER_API_KEY|ANTHROPIC_API_KEY|api[_-]?key)\s*[:=]\s*\S+/gi, "$1=[redacted]");
|
|
1315
|
+
return redactSecrets(stripped).slice(0, 500);
|
|
1316
|
+
}
|
|
1317
|
+
function catalogCanonicalPayload(pin) {
|
|
1318
|
+
return canonicalJson({
|
|
1319
|
+
recordedAt: pin.recordedAt,
|
|
1320
|
+
source: pin.source,
|
|
1321
|
+
units: pin.units,
|
|
1322
|
+
models: pin.models
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
function catalogHash(pin) {
|
|
1326
|
+
return `sha256:${createHash2("sha256").update(catalogCanonicalPayload(pin)).digest("hex")}`;
|
|
1327
|
+
}
|
|
1328
|
+
function parseCatalogPin(raw, nowMs = Date.now()) {
|
|
1329
|
+
if (!isRecord(raw)) return { ok: false, reason: "catalog is not an object" };
|
|
1330
|
+
if (raw.schema !== NOUS_CATALOG_SCHEMA) {
|
|
1331
|
+
return { ok: false, reason: `catalog schema must be ${NOUS_CATALOG_SCHEMA}` };
|
|
1332
|
+
}
|
|
1333
|
+
if (typeof raw.recordedAt !== "string" || Number.isNaN(Date.parse(raw.recordedAt))) {
|
|
1334
|
+
return { ok: false, reason: "catalog recordedAt must be an ISO-8601 timestamp" };
|
|
1335
|
+
}
|
|
1336
|
+
if (typeof raw.source !== "string" || raw.source.trim().length === 0) {
|
|
1337
|
+
return { ok: false, reason: "catalog source is required" };
|
|
1338
|
+
}
|
|
1339
|
+
if (raw.units !== NOUS_PRICE_UNITS) {
|
|
1340
|
+
return { ok: false, reason: `catalog units must be ${NOUS_PRICE_UNITS}` };
|
|
1341
|
+
}
|
|
1342
|
+
if (typeof raw.hash !== "string" || !/^sha256:[a-f0-9]{64}$/.test(raw.hash)) {
|
|
1343
|
+
return { ok: false, reason: "catalog hash must be sha256:<64 hex>" };
|
|
1344
|
+
}
|
|
1345
|
+
if (!isRecord(raw.models)) return { ok: false, reason: "catalog models must be an object" };
|
|
1346
|
+
const expected = catalogHash({
|
|
1347
|
+
recordedAt: raw.recordedAt,
|
|
1348
|
+
source: raw.source.trim(),
|
|
1349
|
+
units: NOUS_PRICE_UNITS,
|
|
1350
|
+
models: raw.models
|
|
1351
|
+
});
|
|
1352
|
+
if (raw.hash !== expected) {
|
|
1353
|
+
return { ok: false, reason: "catalog hash does not match recordedAt/source/units/models" };
|
|
1354
|
+
}
|
|
1355
|
+
const models = {};
|
|
1356
|
+
for (const [id, entry] of Object.entries(raw.models)) {
|
|
1357
|
+
const parsed = parsePinModel(id, entry);
|
|
1358
|
+
if (!parsed.ok) return parsed;
|
|
1359
|
+
models[id] = parsed.model;
|
|
1360
|
+
}
|
|
1361
|
+
const pin = {
|
|
1362
|
+
schema: NOUS_CATALOG_SCHEMA,
|
|
1363
|
+
recordedAt: raw.recordedAt,
|
|
1364
|
+
source: raw.source.trim(),
|
|
1365
|
+
units: NOUS_PRICE_UNITS,
|
|
1366
|
+
hash: raw.hash,
|
|
1367
|
+
models
|
|
1368
|
+
};
|
|
1369
|
+
const recordedAtMs = Date.parse(pin.recordedAt);
|
|
1370
|
+
if (recordedAtMs > nowMs + 6e4) {
|
|
1371
|
+
return { ok: false, reason: "catalog recordedAt is in the future" };
|
|
1372
|
+
}
|
|
1373
|
+
if (nowMs - recordedAtMs > NOUS_CATALOG_MAX_AGE_MS) {
|
|
1374
|
+
return { ok: false, reason: "catalog is stale (recordedAt older than 30 days)" };
|
|
1375
|
+
}
|
|
1376
|
+
return { ok: true, pin };
|
|
1377
|
+
}
|
|
1378
|
+
function parsePinModel(id, entry) {
|
|
1379
|
+
if (!id.trim()) return { ok: false, reason: "catalog model id is empty" };
|
|
1380
|
+
if (!isRecord(entry)) return { ok: false, reason: `catalog model ${id} is not an object` };
|
|
1381
|
+
const contextWindow = asPositiveInt(entry.contextWindow);
|
|
1382
|
+
const maxTokens = asPositiveInt(entry.maxTokens);
|
|
1383
|
+
if (contextWindow === void 0 || maxTokens === void 0) {
|
|
1384
|
+
return { ok: false, reason: `catalog model ${id} is missing positive contextWindow/maxTokens` };
|
|
1385
|
+
}
|
|
1386
|
+
if (entry.priceBasis !== "list" && entry.priceBasis !== "upper-bound") {
|
|
1387
|
+
return { ok: false, reason: `catalog model ${id} priceBasis must be list or upper-bound` };
|
|
1388
|
+
}
|
|
1389
|
+
if (entry.billing !== "metered" && entry.billing !== "subscription" && entry.billing !== "subscription_plus_usage") {
|
|
1390
|
+
return { ok: false, reason: `catalog model ${id} billing must be metered, subscription, or subscription_plus_usage` };
|
|
1391
|
+
}
|
|
1392
|
+
if (typeof entry.verified !== "boolean") {
|
|
1393
|
+
return { ok: false, reason: `catalog model ${id} verified must be a boolean` };
|
|
1394
|
+
}
|
|
1395
|
+
const rates = parseRates(entry.cost);
|
|
1396
|
+
if (!rates) return { ok: false, reason: `catalog model ${id} cost rates must be finite nonnegative numbers` };
|
|
1397
|
+
let cost = rates;
|
|
1398
|
+
let priceBasis = entry.priceBasis;
|
|
1399
|
+
if (entry.tiers !== void 0) {
|
|
1400
|
+
if (!Array.isArray(entry.tiers) || entry.tiers.length === 0) {
|
|
1401
|
+
return { ok: false, reason: `catalog model ${id} tiers must be a nonempty array when present` };
|
|
1402
|
+
}
|
|
1403
|
+
const tierRates = [rates];
|
|
1404
|
+
for (const tier of entry.tiers) {
|
|
1405
|
+
if (!isRecord(tier)) return { ok: false, reason: `catalog model ${id} has a malformed tier` };
|
|
1406
|
+
const parsedTier = parseRates(tier);
|
|
1407
|
+
if (!parsedTier) return { ok: false, reason: `catalog model ${id} tier rates must be finite nonnegative numbers` };
|
|
1408
|
+
if (asNonnegInt(tier.inputTokensAbove) === void 0) {
|
|
1409
|
+
return { ok: false, reason: `catalog model ${id} tier is missing inputTokensAbove` };
|
|
1410
|
+
}
|
|
1411
|
+
tierRates.push(parsedTier);
|
|
1412
|
+
}
|
|
1413
|
+
cost = upperBoundRates(tierRates);
|
|
1414
|
+
priceBasis = "upper-bound";
|
|
1415
|
+
}
|
|
1416
|
+
if (!entry.verified && hasZeroRate(cost)) {
|
|
1417
|
+
return { ok: false, reason: `catalog model ${id} has unverified zero rates` };
|
|
1418
|
+
}
|
|
1419
|
+
return {
|
|
1420
|
+
ok: true,
|
|
1421
|
+
model: {
|
|
1422
|
+
contextWindow,
|
|
1423
|
+
maxTokens,
|
|
1424
|
+
cost,
|
|
1425
|
+
priceBasis,
|
|
1426
|
+
billing: entry.billing,
|
|
1427
|
+
verified: entry.verified,
|
|
1428
|
+
...typeof entry.name === "string" && entry.name.trim() ? { name: entry.name.trim() } : {}
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function parseModelsResponse(raw) {
|
|
1433
|
+
if (!isRecord(raw) || !Array.isArray(raw.data)) {
|
|
1434
|
+
return { ok: false, error: "invalid", reason: "models response is not an object with a data array" };
|
|
1435
|
+
}
|
|
1436
|
+
const models = [];
|
|
1437
|
+
for (const item of raw.data) {
|
|
1438
|
+
if (!isRecord(item) || typeof item.id !== "string" || !item.id.trim()) {
|
|
1439
|
+
return { ok: false, error: "invalid", reason: "models response contains an entry without an id" };
|
|
1440
|
+
}
|
|
1441
|
+
const model = { id: item.id.trim() };
|
|
1442
|
+
if (typeof item.name === "string" && item.name.trim()) model.name = item.name.trim();
|
|
1443
|
+
const topProvider = isRecord(item.top_provider) ? item.top_provider : void 0;
|
|
1444
|
+
const contextWindow = asPositiveInt(item.contextWindow ?? item.context_window ?? item.context_length);
|
|
1445
|
+
const maxTokens = asPositiveInt(
|
|
1446
|
+
item.maxTokens ?? item.max_tokens ?? item.max_output_tokens ?? topProvider?.max_completion_tokens
|
|
1447
|
+
);
|
|
1448
|
+
if (contextWindow !== void 0) model.contextWindow = contextWindow;
|
|
1449
|
+
if (maxTokens !== void 0) model.maxTokens = maxTokens;
|
|
1450
|
+
if (typeof item.units === "string") model.units = item.units;
|
|
1451
|
+
if (typeof item.verified === "boolean") model.verified = item.verified;
|
|
1452
|
+
const architecture = isRecord(item.architecture) ? item.architecture : void 0;
|
|
1453
|
+
if (architecture && "input_modalities" in architecture) {
|
|
1454
|
+
const input = parseInputModalities(architecture.input_modalities);
|
|
1455
|
+
if (!input) model.capabilityIssue = "unknown input modalities";
|
|
1456
|
+
else model.input = input;
|
|
1457
|
+
}
|
|
1458
|
+
const supported = parseSupportedParameters(item.supported_parameters);
|
|
1459
|
+
if (supported) {
|
|
1460
|
+
model.reasoning = supported.reasoning;
|
|
1461
|
+
model.tools = supported.tools;
|
|
1462
|
+
}
|
|
1463
|
+
if (isRecord(item.pricing)) {
|
|
1464
|
+
const live = parseLivePricing(item.pricing);
|
|
1465
|
+
if (!live) {
|
|
1466
|
+
model.pricingIssue = "malformed or incomplete live pricing; costly tiers are not dropped and zeros are not guessed";
|
|
1467
|
+
} else {
|
|
1468
|
+
model.cost = live.cost;
|
|
1469
|
+
model.units = NOUS_PRICE_UNITS;
|
|
1470
|
+
if (live.tiers.length > 0) model.tiers = live.tiers;
|
|
1471
|
+
}
|
|
1472
|
+
} else {
|
|
1473
|
+
const cost = isRecord(item.cost) ? parseRates(item.cost) : void 0;
|
|
1474
|
+
if (cost) model.cost = cost;
|
|
1475
|
+
if (Array.isArray(item.tiers)) {
|
|
1476
|
+
const tiers = [];
|
|
1477
|
+
for (const tier of item.tiers) {
|
|
1478
|
+
if (!isRecord(tier)) {
|
|
1479
|
+
model.pricingIssue = "malformed assumed-shape tier";
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
const rates = parseRates(tier);
|
|
1483
|
+
const above = asNonnegInt(tier.inputTokensAbove ?? tier.input_tokens_above);
|
|
1484
|
+
if (!rates || above === void 0) {
|
|
1485
|
+
model.pricingIssue = "incomplete assumed-shape tier";
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
tiers.push({ ...rates, inputTokensAbove: above });
|
|
1489
|
+
}
|
|
1490
|
+
if (!model.pricingIssue && tiers.length > 0) model.tiers = tiers;
|
|
1491
|
+
if (model.pricingIssue) {
|
|
1492
|
+
delete model.cost;
|
|
1493
|
+
delete model.tiers;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
models.push(model);
|
|
1498
|
+
}
|
|
1499
|
+
return { ok: true, models };
|
|
1500
|
+
}
|
|
1501
|
+
async function fetchNousModels(options) {
|
|
1502
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1503
|
+
const controller = new AbortController();
|
|
1504
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
1505
|
+
try {
|
|
1506
|
+
const headers = { Accept: "application/json" };
|
|
1507
|
+
if (options.authorization) headers.Authorization = `Bearer ${options.authorization}`;
|
|
1508
|
+
const response = await fetchImpl(options.url, { method: "GET", headers, signal: controller.signal });
|
|
1509
|
+
if (response.status === 401 || response.status === 403) {
|
|
1510
|
+
return { ok: false, error: "auth", status: response.status, reason: `HTTP ${response.status}` };
|
|
1511
|
+
}
|
|
1512
|
+
if (!response.ok) {
|
|
1513
|
+
return { ok: false, error: "http", status: response.status, reason: `HTTP ${response.status}` };
|
|
1514
|
+
}
|
|
1515
|
+
let text;
|
|
1516
|
+
try {
|
|
1517
|
+
text = await response.text();
|
|
1518
|
+
} catch {
|
|
1519
|
+
return { ok: false, error: "invalid", reason: "models response is not JSON" };
|
|
1520
|
+
}
|
|
1521
|
+
let payload;
|
|
1522
|
+
try {
|
|
1523
|
+
payload = JSON.parse(text);
|
|
1524
|
+
} catch {
|
|
1525
|
+
return { ok: false, error: "invalid", reason: "models response is not JSON" };
|
|
1526
|
+
}
|
|
1527
|
+
const parsed = parseModelsResponse(payload);
|
|
1528
|
+
if (!parsed.ok) return parsed;
|
|
1529
|
+
return {
|
|
1530
|
+
...parsed,
|
|
1531
|
+
provenance: {
|
|
1532
|
+
source: options.url,
|
|
1533
|
+
fetchedAt: new Date(options.nowMs ?? Date.now()).toISOString(),
|
|
1534
|
+
rawSha256: `sha256:${createHash2("sha256").update(text).digest("hex")}`
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
if (isAbortError(error)) {
|
|
1539
|
+
return { ok: false, error: "timeout", reason: `discovery timed out after ${options.timeoutMs}ms` };
|
|
1540
|
+
}
|
|
1541
|
+
if (isConnectionRefused(error)) {
|
|
1542
|
+
return { ok: false, error: "connection-refused", reason: "connection refused" };
|
|
1543
|
+
}
|
|
1544
|
+
return { ok: false, error: "http", reason: sanitizeNousText(error instanceof Error ? error.message : "discovery failed") };
|
|
1545
|
+
} finally {
|
|
1546
|
+
clearTimeout(timer);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
function buildModelConfigs(discovered, pin, route) {
|
|
1550
|
+
const registered = [];
|
|
1551
|
+
const skipped = [];
|
|
1552
|
+
const requiredBilling = route === "direct" ? ["metered"] : ["subscription", "subscription_plus_usage"];
|
|
1553
|
+
for (const item of discovered) {
|
|
1554
|
+
if (item.capabilityIssue) {
|
|
1555
|
+
skipped.push({ id: item.id, reason: `excluded: ${item.capabilityIssue}` });
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
const fromPin = pin?.models[item.id];
|
|
1559
|
+
if (item.pricingIssue && !fromPin) {
|
|
1560
|
+
skipped.push({ id: item.id, reason: `unpriced, not registered: ${item.pricingIssue}` });
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
const capacity = {
|
|
1564
|
+
contextWindow: fromPin?.contextWindow ?? item.contextWindow,
|
|
1565
|
+
maxTokens: fromPin?.maxTokens ?? item.maxTokens
|
|
1566
|
+
};
|
|
1567
|
+
if (capacity.contextWindow === void 0 || capacity.maxTokens === void 0) {
|
|
1568
|
+
skipped.push({ id: item.id, reason: "unpriced, not registered: missing contextWindow or maxTokens" });
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
let cost;
|
|
1572
|
+
let priceBasis = "list";
|
|
1573
|
+
let verified = false;
|
|
1574
|
+
let tiers;
|
|
1575
|
+
if (fromPin) {
|
|
1576
|
+
cost = fromPin.cost;
|
|
1577
|
+
priceBasis = fromPin.priceBasis;
|
|
1578
|
+
verified = fromPin.verified;
|
|
1579
|
+
} else {
|
|
1580
|
+
const rateSources = [];
|
|
1581
|
+
if (item.cost) rateSources.push(item.cost);
|
|
1582
|
+
if (item.tiers && item.tiers.length > 0) rateSources.push(...item.tiers);
|
|
1583
|
+
if (rateSources.length === 0) {
|
|
1584
|
+
skipped.push({ id: item.id, reason: "unpriced, not registered: no numeric cost rates and no catalog pin" });
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
if (item.units !== NOUS_PRICE_UNITS) {
|
|
1588
|
+
skipped.push({ id: item.id, reason: `unpriced, not registered: units must be ${NOUS_PRICE_UNITS}` });
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
if (item.tiers && item.tiers.length > 0) {
|
|
1592
|
+
priceBasis = "upper-bound";
|
|
1593
|
+
cost = upperBoundRates(rateSources);
|
|
1594
|
+
tiers = item.tiers;
|
|
1595
|
+
} else {
|
|
1596
|
+
cost = item.cost;
|
|
1597
|
+
}
|
|
1598
|
+
verified = item.verified === true;
|
|
1599
|
+
}
|
|
1600
|
+
if (!cost) {
|
|
1601
|
+
skipped.push({ id: item.id, reason: "unpriced, not registered" });
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
if (hasZeroRate(cost) && !verified) {
|
|
1605
|
+
skipped.push({ id: item.id, reason: "unpriced, not registered: unverified zero rates" });
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
const billing = fromPin?.billing ?? (route === "direct" ? "metered" : "subscription");
|
|
1609
|
+
if (!requiredBilling.includes(billing)) {
|
|
1610
|
+
skipped.push({ id: item.id, reason: `excluded: billing ${billing} is not valid for ${route}` });
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
const display = labeledModelName(fromPin?.name ?? item.name ?? item.id, route, priceBasis);
|
|
1614
|
+
registered.push({
|
|
1615
|
+
id: item.id,
|
|
1616
|
+
name: display,
|
|
1617
|
+
contextWindow: capacity.contextWindow,
|
|
1618
|
+
maxTokens: capacity.maxTokens,
|
|
1619
|
+
cost,
|
|
1620
|
+
priceBasis,
|
|
1621
|
+
billing,
|
|
1622
|
+
verified,
|
|
1623
|
+
...item.input ? { input: item.input } : {},
|
|
1624
|
+
...item.reasoning !== void 0 ? { reasoning: item.reasoning } : {},
|
|
1625
|
+
...item.tools !== void 0 ? { tools: item.tools } : {},
|
|
1626
|
+
...tiers ? { tiers } : {}
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
return { registered, skipped };
|
|
1630
|
+
}
|
|
1631
|
+
function guidanceFor(input) {
|
|
1632
|
+
const messages = [];
|
|
1633
|
+
if (input.parsed.status === "invalid") {
|
|
1634
|
+
messages.push({ message: input.parsed.error, level: "error" });
|
|
1635
|
+
return messages;
|
|
1636
|
+
}
|
|
1637
|
+
if (input.parsed.status !== "ready") return messages;
|
|
1638
|
+
if (input.parsed.providers.includes("direct") && !input.directHasKey) {
|
|
1639
|
+
messages.push({
|
|
1640
|
+
message: "Nous direct API is opted in but NOUS_API_KEY is unset. Set NOUS_API_KEY. This slice does not use stored Pi /login credentials.",
|
|
1641
|
+
level: "info"
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
if (input.proxyUrlInvalid) {
|
|
1645
|
+
messages.push({
|
|
1646
|
+
message: "KXM_NOUS_PROXY_URL must be a loopback http(s) URL (127.0.0.1, localhost, or ::1). Failing closed.",
|
|
1647
|
+
level: "error"
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
if (input.proxyDiscovery?.ok === false && input.proxyDiscovery.error === "connection-refused") {
|
|
1651
|
+
messages.push({
|
|
1652
|
+
message: "Nous proxy is not reachable. Start it with `hermes proxy start` and check `hermes proxy status`. KXM does not install, spawn, or log in for you.",
|
|
1653
|
+
level: "info"
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
if (input.proxyDiscovery?.ok === false && input.proxyDiscovery.error === "auth") {
|
|
1657
|
+
messages.push({
|
|
1658
|
+
message: "Nous proxy returned unauthorized. Log in with `hermes login --provider nous`. Newer docs also mention `hermes setup --portal` (not verified on this CLI).",
|
|
1659
|
+
level: "info"
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
if (input.directDiscovery?.ok === false && input.directDiscovery.error === "timeout") {
|
|
1663
|
+
messages.push({ message: `Nous direct discovery ${input.directDiscovery.reason}.`, level: "info" });
|
|
1664
|
+
}
|
|
1665
|
+
if (input.proxyDiscovery?.ok === false && input.proxyDiscovery.error === "timeout") {
|
|
1666
|
+
messages.push({ message: `Nous proxy discovery ${input.proxyDiscovery.reason}.`, level: "info" });
|
|
1667
|
+
}
|
|
1668
|
+
if (input.catalogError) {
|
|
1669
|
+
messages.push({ message: `Nous catalog pin not used: ${input.catalogError}.`, level: "error" });
|
|
1670
|
+
}
|
|
1671
|
+
if (input.skipped && input.skipped.length > 0) {
|
|
1672
|
+
const preview = input.skipped.slice(0, 8).map((item) => `${item.id} (${item.reason})`).join("; ");
|
|
1673
|
+
messages.push({
|
|
1674
|
+
message: `Skipped ${input.skipped.length} Nous model(s) as unpriced or invalid: ${preview}.`,
|
|
1675
|
+
level: "info"
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
return messages.map((item) => ({ ...item, message: sanitizeNousText(item.message) }));
|
|
1679
|
+
}
|
|
1680
|
+
function labeledModelName(base, route, priceBasis) {
|
|
1681
|
+
if (route === "proxy") {
|
|
1682
|
+
return priceBasis === "upper-bound" ? `${base} (subscription proxy, market ref; upper-bound)` : `${base} (subscription proxy, market ref)`;
|
|
1683
|
+
}
|
|
1684
|
+
return priceBasis === "upper-bound" ? `${base} (upper-bound market ref)` : base;
|
|
1685
|
+
}
|
|
1686
|
+
function parseLivePricing(pricing) {
|
|
1687
|
+
const cost = parseLiveRates(pricing);
|
|
1688
|
+
if (!cost) return void 0;
|
|
1689
|
+
if (!("overrides" in pricing) || pricing.overrides === void 0) {
|
|
1690
|
+
return { cost, tiers: [] };
|
|
1691
|
+
}
|
|
1692
|
+
if (!Array.isArray(pricing.overrides)) return void 0;
|
|
1693
|
+
const tiers = [];
|
|
1694
|
+
for (const override of pricing.overrides) {
|
|
1695
|
+
if (!isRecord(override)) return void 0;
|
|
1696
|
+
const rates = parseLiveRates(override);
|
|
1697
|
+
const above = asNonnegInt(override.min_prompt_tokens ?? override.minPromptTokens);
|
|
1698
|
+
if (!rates || above === void 0) return void 0;
|
|
1699
|
+
tiers.push({ ...rates, inputTokensAbove: above });
|
|
1700
|
+
}
|
|
1701
|
+
return { cost, tiers };
|
|
1702
|
+
}
|
|
1703
|
+
function parseLiveRates(value) {
|
|
1704
|
+
const input = perTokenToUsdPerM(value.prompt);
|
|
1705
|
+
const output = perTokenToUsdPerM(value.completion);
|
|
1706
|
+
const cacheRead = perTokenToUsdPerM(value.input_cache_read ?? value.inputCacheRead);
|
|
1707
|
+
const cacheWrite = perTokenToUsdPerM(value.input_cache_write ?? value.inputCacheWrite);
|
|
1708
|
+
if (input === void 0 || output === void 0 || cacheRead === void 0 || cacheWrite === void 0) return void 0;
|
|
1709
|
+
return { input, output, cacheRead, cacheWrite };
|
|
1710
|
+
}
|
|
1711
|
+
function perTokenToUsdPerM(value) {
|
|
1712
|
+
let perToken;
|
|
1713
|
+
if (typeof value === "number") perToken = value;
|
|
1714
|
+
else if (typeof value === "string" && value.trim() !== "") perToken = Number(value);
|
|
1715
|
+
if (perToken === void 0 || !Number.isFinite(perToken) || perToken < 0) return void 0;
|
|
1716
|
+
const usdPerM = Number((perToken * PER_TOKEN_TO_USD_PER_M).toFixed(8));
|
|
1717
|
+
if (!Number.isFinite(usdPerM) || usdPerM < 0) return void 0;
|
|
1718
|
+
return usdPerM;
|
|
1719
|
+
}
|
|
1720
|
+
function parseInputModalities(value) {
|
|
1721
|
+
if (!Array.isArray(value)) return void 0;
|
|
1722
|
+
const present = new Set(value.filter((item) => item === "text" || item === "image"));
|
|
1723
|
+
const mapped = PI_INPUT_ORDER.filter((item) => present.has(item));
|
|
1724
|
+
return mapped.length > 0 ? mapped : void 0;
|
|
1725
|
+
}
|
|
1726
|
+
function parseSupportedParameters(value) {
|
|
1727
|
+
if (!Array.isArray(value)) return void 0;
|
|
1728
|
+
const params = new Set(value.filter((item) => typeof item === "string"));
|
|
1729
|
+
return {
|
|
1730
|
+
reasoning: params.has("reasoning") || params.has("include_reasoning"),
|
|
1731
|
+
tools: params.has("tools") || params.has("tool_choice")
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
function parseRates(value) {
|
|
1735
|
+
if (!isRecord(value)) return void 0;
|
|
1736
|
+
const input = asFiniteNonneg(value.input);
|
|
1737
|
+
const output = asFiniteNonneg(value.output);
|
|
1738
|
+
const cacheRead = asFiniteNonneg(value.cacheRead ?? value.cache_read ?? value.cache_input);
|
|
1739
|
+
const cacheWrite = asFiniteNonneg(value.cacheWrite ?? value.cache_write);
|
|
1740
|
+
if (input === void 0 || output === void 0 || cacheRead === void 0 || cacheWrite === void 0) return void 0;
|
|
1741
|
+
return { input, output, cacheRead, cacheWrite };
|
|
1742
|
+
}
|
|
1743
|
+
function upperBoundRates(list) {
|
|
1744
|
+
return {
|
|
1745
|
+
input: Math.max(...list.map((item) => item.input)),
|
|
1746
|
+
output: Math.max(...list.map((item) => item.output)),
|
|
1747
|
+
cacheRead: Math.max(...list.map((item) => item.cacheRead)),
|
|
1748
|
+
cacheWrite: Math.max(...list.map((item) => item.cacheWrite))
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
function hasZeroRate(cost) {
|
|
1752
|
+
return cost.input === 0 || cost.output === 0 || cost.cacheRead === 0 || cost.cacheWrite === 0;
|
|
1753
|
+
}
|
|
1754
|
+
function asFiniteNonneg(value) {
|
|
1755
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
|
|
1756
|
+
return value;
|
|
1757
|
+
}
|
|
1758
|
+
function asPositiveInt(value) {
|
|
1759
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return void 0;
|
|
1760
|
+
return value;
|
|
1761
|
+
}
|
|
1762
|
+
function asNonnegInt(value) {
|
|
1763
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) return void 0;
|
|
1764
|
+
return value;
|
|
1765
|
+
}
|
|
1766
|
+
function isRecord(value) {
|
|
1767
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1768
|
+
}
|
|
1769
|
+
function canonicalJson(value) {
|
|
1770
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1771
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
1772
|
+
const keys = Object.keys(value).sort();
|
|
1773
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
1774
|
+
}
|
|
1775
|
+
function isAbortError(error) {
|
|
1776
|
+
if (!error || typeof error !== "object") return false;
|
|
1777
|
+
const name = error.name;
|
|
1778
|
+
return name === "AbortError" || name === "TimeoutError";
|
|
1779
|
+
}
|
|
1780
|
+
function isConnectionRefused(error) {
|
|
1781
|
+
const codes = [];
|
|
1782
|
+
let current = error;
|
|
1783
|
+
for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
|
|
1784
|
+
const code = current.code;
|
|
1785
|
+
if (typeof code === "string") codes.push(code);
|
|
1786
|
+
const message = current.message;
|
|
1787
|
+
if (typeof message === "string" && /ECONNREFUSED/i.test(message)) return true;
|
|
1788
|
+
current = current.cause;
|
|
1789
|
+
}
|
|
1790
|
+
return codes.includes("ECONNREFUSED");
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// plugins/kxm/src/nous-pi.ts
|
|
1794
|
+
var emptySide = (id) => ({ id, registered: 0, skipped: 0 });
|
|
1795
|
+
function emptyNousReport() {
|
|
1796
|
+
return {
|
|
1797
|
+
optedIn: false,
|
|
1798
|
+
direct: emptySide(NOUS_DIRECT_ID),
|
|
1799
|
+
proxy: emptySide(NOUS_PROXY_ID),
|
|
1800
|
+
guidance: [],
|
|
1801
|
+
registeredProviders: []
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
function nousFactoryWork(pi, onReport, deps = {}) {
|
|
1805
|
+
const env = deps.env ?? process.env;
|
|
1806
|
+
const parsed = parseNousEnv(env);
|
|
1807
|
+
if (parsed.status === "unset") {
|
|
1808
|
+
onReport(emptyNousReport());
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
if (parsed.status === "invalid") {
|
|
1812
|
+
onReport({
|
|
1813
|
+
...emptyNousReport(),
|
|
1814
|
+
optedIn: true,
|
|
1815
|
+
guidance: guidanceFor({ parsed })
|
|
1816
|
+
});
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
return registerNousProviders(pi, deps).then(onReport);
|
|
1820
|
+
}
|
|
1821
|
+
async function registerNousProviders(pi, deps = {}) {
|
|
1822
|
+
const env = deps.env ?? process.env;
|
|
1823
|
+
const parsed = parseNousEnv(env);
|
|
1824
|
+
if (parsed.status === "unset") return emptyNousReport();
|
|
1825
|
+
if (parsed.status === "invalid") {
|
|
1826
|
+
return { ...emptyNousReport(), optedIn: true, guidance: guidanceFor({ parsed }) };
|
|
1827
|
+
}
|
|
1828
|
+
const report = emptyNousReport();
|
|
1829
|
+
report.optedIn = true;
|
|
1830
|
+
let pin;
|
|
1831
|
+
let catalogError;
|
|
1832
|
+
if (parsed.catalogFile) {
|
|
1833
|
+
const loaded = await loadCatalog(parsed.catalogFile, deps);
|
|
1834
|
+
if (loaded.ok) pin = loaded.pin;
|
|
1835
|
+
else catalogError = loaded.reason;
|
|
1836
|
+
}
|
|
1837
|
+
const skipped = [];
|
|
1838
|
+
let directDiscovery;
|
|
1839
|
+
let proxyDiscovery;
|
|
1840
|
+
let proxyUrlInvalid = false;
|
|
1841
|
+
const directHasKey = hasDirectKey(env);
|
|
1842
|
+
if (parsed.providers.includes("direct")) {
|
|
1843
|
+
const result2 = await registerRoute({
|
|
1844
|
+
pi,
|
|
1845
|
+
route: "direct",
|
|
1846
|
+
env,
|
|
1847
|
+
parsed,
|
|
1848
|
+
pin,
|
|
1849
|
+
hasKey: directHasKey,
|
|
1850
|
+
...deps.fetch ? { fetchImpl: deps.fetch } : {},
|
|
1851
|
+
...deps.nowMs !== void 0 ? { nowMs: deps.nowMs } : {}
|
|
1852
|
+
});
|
|
1853
|
+
report.direct = result2.side;
|
|
1854
|
+
directDiscovery = result2.discovery;
|
|
1855
|
+
skipped.push(...result2.skipped);
|
|
1856
|
+
if (result2.registered) report.registeredProviders.push(NOUS_DIRECT_ID);
|
|
1857
|
+
}
|
|
1858
|
+
if (parsed.providers.includes("proxy")) {
|
|
1859
|
+
if (!isLoopbackUrl(parsed.proxyUrl)) {
|
|
1860
|
+
proxyUrlInvalid = true;
|
|
1861
|
+
report.proxy = {
|
|
1862
|
+
id: NOUS_PROXY_ID,
|
|
1863
|
+
registered: 0,
|
|
1864
|
+
skipped: 0,
|
|
1865
|
+
error: "non-loopback",
|
|
1866
|
+
reason: "proxy URL is not loopback http(s)"
|
|
1867
|
+
};
|
|
1868
|
+
} else {
|
|
1869
|
+
const result2 = await registerRoute({
|
|
1870
|
+
pi,
|
|
1871
|
+
route: "proxy",
|
|
1872
|
+
env,
|
|
1873
|
+
parsed,
|
|
1874
|
+
pin,
|
|
1875
|
+
hasKey: true,
|
|
1876
|
+
...deps.fetch ? { fetchImpl: deps.fetch } : {},
|
|
1877
|
+
...deps.nowMs !== void 0 ? { nowMs: deps.nowMs } : {}
|
|
1878
|
+
});
|
|
1879
|
+
report.proxy = result2.side;
|
|
1880
|
+
proxyDiscovery = result2.discovery;
|
|
1881
|
+
skipped.push(...result2.skipped);
|
|
1882
|
+
if (result2.registered) report.registeredProviders.push(NOUS_PROXY_ID);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
report.guidance = guidanceFor({
|
|
1886
|
+
parsed,
|
|
1887
|
+
directHasKey,
|
|
1888
|
+
proxyUrlInvalid,
|
|
1889
|
+
skipped,
|
|
1890
|
+
...directDiscovery ? { directDiscovery } : {},
|
|
1891
|
+
...proxyDiscovery ? { proxyDiscovery } : {},
|
|
1892
|
+
...catalogError ? { catalogError } : {}
|
|
1893
|
+
});
|
|
1894
|
+
return report;
|
|
1895
|
+
}
|
|
1896
|
+
async function registerRoute(input) {
|
|
1897
|
+
const providerId = input.route === "direct" ? NOUS_DIRECT_ID : NOUS_PROXY_ID;
|
|
1898
|
+
const baseUrl = input.route === "direct" ? NOUS_DIRECT_BASE_URL : input.parsed.proxyUrl;
|
|
1899
|
+
const side = { id: providerId, registered: 0, skipped: 0 };
|
|
1900
|
+
if (input.route === "direct" && !input.hasKey) {
|
|
1901
|
+
registerLegacy(input.pi, providerId, baseUrl, "$NOUS_API_KEY", []);
|
|
1902
|
+
return { side, skipped: [], registered: true };
|
|
1903
|
+
}
|
|
1904
|
+
const authorization = input.route === "direct" ? input.env.NOUS_API_KEY : NOUS_PROXY_PLACEHOLDER_KEY;
|
|
1905
|
+
const discovery = await fetchNousModels({
|
|
1906
|
+
url: modelsUrl(baseUrl),
|
|
1907
|
+
timeoutMs: input.parsed.timeoutMs,
|
|
1908
|
+
...authorization ? { authorization } : {},
|
|
1909
|
+
...input.fetchImpl ? { fetchImpl: input.fetchImpl } : {},
|
|
1910
|
+
...input.nowMs !== void 0 ? { nowMs: input.nowMs } : {}
|
|
1911
|
+
});
|
|
1912
|
+
if (!discovery.ok) {
|
|
1913
|
+
side.error = discovery.error;
|
|
1914
|
+
side.reason = discovery.reason;
|
|
1915
|
+
registerLegacy(input.pi, providerId, baseUrl, apiKeyRef(input.route), []);
|
|
1916
|
+
return { side, discovery, skipped: [], registered: true };
|
|
1917
|
+
}
|
|
1918
|
+
if (discovery.provenance) side.provenance = discovery.provenance;
|
|
1919
|
+
const built = buildModelConfigs(discovery.models, input.pin, input.route);
|
|
1920
|
+
side.registered = built.registered.length;
|
|
1921
|
+
side.skipped = built.skipped.length;
|
|
1922
|
+
registerLegacy(input.pi, providerId, baseUrl, apiKeyRef(input.route), built.registered);
|
|
1923
|
+
return { side, discovery, skipped: built.skipped, registered: true };
|
|
1924
|
+
}
|
|
1925
|
+
function apiKeyRef(route) {
|
|
1926
|
+
return route === "direct" ? "$NOUS_API_KEY" : NOUS_PROXY_PLACEHOLDER_KEY;
|
|
1927
|
+
}
|
|
1928
|
+
function registerLegacy(pi, id, baseUrl, apiKey, models) {
|
|
1929
|
+
if (typeof pi.registerProvider !== "function") {
|
|
1930
|
+
throw new Error("Pi registerProvider is unavailable");
|
|
1931
|
+
}
|
|
1932
|
+
pi.registerProvider(id, {
|
|
1933
|
+
name: id === NOUS_DIRECT_ID ? "Nous" : "Nous subscription proxy",
|
|
1934
|
+
baseUrl,
|
|
1935
|
+
apiKey,
|
|
1936
|
+
api: "openai-completions",
|
|
1937
|
+
authHeader: true,
|
|
1938
|
+
models: models.map((model) => ({
|
|
1939
|
+
id: model.id,
|
|
1940
|
+
name: model.name,
|
|
1941
|
+
reasoning: model.reasoning === true,
|
|
1942
|
+
input: model.input && model.input.length > 0 ? model.input : ["text"],
|
|
1943
|
+
// Upper-bound rates only: Pi cost.tiers uses strict > and can underquote.
|
|
1944
|
+
cost: {
|
|
1945
|
+
input: model.cost.input,
|
|
1946
|
+
output: model.cost.output,
|
|
1947
|
+
cacheRead: model.cost.cacheRead,
|
|
1948
|
+
cacheWrite: model.cost.cacheWrite
|
|
1949
|
+
},
|
|
1950
|
+
contextWindow: model.contextWindow,
|
|
1951
|
+
maxTokens: model.maxTokens
|
|
1952
|
+
}))
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
function hasDirectKey(env) {
|
|
1956
|
+
const key = env.NOUS_API_KEY;
|
|
1957
|
+
return typeof key === "string" && key.trim().length > 0;
|
|
1958
|
+
}
|
|
1959
|
+
async function loadCatalog(path, deps) {
|
|
1960
|
+
try {
|
|
1961
|
+
const read = deps.readFile ?? ((filePath) => readFile(filePath, "utf8"));
|
|
1962
|
+
const text = await read(path);
|
|
1963
|
+
return parseCatalogPin(JSON.parse(text), deps.nowMs ?? Date.now());
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
if (error instanceof SyntaxError) {
|
|
1966
|
+
return { ok: false, reason: "catalog file is not valid JSON" };
|
|
1967
|
+
}
|
|
1968
|
+
return { ok: false, reason: sanitizeNousText(error instanceof Error ? error.message : "catalog file could not be read") };
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// plugins/kxm/src/diagnostics.ts
|
|
1973
|
+
var NEXT_ACTIONS = [
|
|
1974
|
+
"use_assigned_coordinator",
|
|
1975
|
+
"wait_then_checkpoint",
|
|
1976
|
+
"record_journal_as_coordinator",
|
|
1977
|
+
"reconnect_with_current_agent_key",
|
|
1978
|
+
"check_project_token",
|
|
1979
|
+
"export_retrospective",
|
|
1980
|
+
"post_signal",
|
|
1981
|
+
"restart_fresh_session",
|
|
1982
|
+
"switch_model_or_retry"
|
|
1983
|
+
];
|
|
1984
|
+
var CODE_TO_CLASS = {
|
|
1985
|
+
workflow_forbidden: "workflow_scope",
|
|
1986
|
+
invalid_agent_identity: "invalid_identity",
|
|
1987
|
+
invalid_auth: "invalid_auth",
|
|
1988
|
+
workflow_signal_mismatch: "signal_mismatch",
|
|
1989
|
+
workflow_not_waiting: "not_waiting",
|
|
1990
|
+
workflow_terminal: "not_waiting",
|
|
1991
|
+
workflow_not_running: "not_waiting",
|
|
1992
|
+
unresumable_session: "unresumable_session",
|
|
1993
|
+
provider_quota: "quota",
|
|
1994
|
+
provider_error: "provider_error"
|
|
1995
|
+
};
|
|
1996
|
+
function operationForTool(toolName) {
|
|
1997
|
+
switch (toolName) {
|
|
1998
|
+
case "kxm_workflow_get":
|
|
1999
|
+
case "kxm_workflow_list":
|
|
2000
|
+
return "get";
|
|
2001
|
+
case "kxm_workflow_wait":
|
|
2002
|
+
return "wait";
|
|
2003
|
+
case "kxm_workflow_checkpoint":
|
|
2004
|
+
return "checkpoint";
|
|
2005
|
+
case "kxm_workflow_record":
|
|
2006
|
+
return "journal";
|
|
2007
|
+
case "kxm_await":
|
|
2008
|
+
return "await";
|
|
2009
|
+
default:
|
|
2010
|
+
return "other";
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
function areaForTool(toolName) {
|
|
2014
|
+
if (toolName?.startsWith("kxm_workflow_")) return "workflow";
|
|
2015
|
+
if (toolName?.startsWith("kxm_")) return "harness";
|
|
2016
|
+
return "implementation";
|
|
2017
|
+
}
|
|
2018
|
+
function nextActionForCode(code, operation) {
|
|
2019
|
+
if (code === "workflow_forbidden") return "use_assigned_coordinator";
|
|
2020
|
+
if (code === "invalid_agent_identity") return "reconnect_with_current_agent_key";
|
|
2021
|
+
if (code === "invalid_auth") return "check_project_token";
|
|
2022
|
+
if (code === "workflow_not_waiting" || code === "workflow_terminal" || code === "workflow_not_running") {
|
|
2023
|
+
return operation === "checkpoint" ? "wait_then_checkpoint" : "export_retrospective";
|
|
2024
|
+
}
|
|
2025
|
+
if (code === "workflow_signal_mismatch") return "post_signal";
|
|
2026
|
+
if (code === "unresumable_session") return "restart_fresh_session";
|
|
2027
|
+
if (code === "provider_quota" || code === "provider_error") return "switch_model_or_retry";
|
|
2028
|
+
return void 0;
|
|
2029
|
+
}
|
|
2030
|
+
function classFromTokens(text) {
|
|
2031
|
+
const value = text.toLowerCase();
|
|
2032
|
+
if (/\b(enoent|not recognized|command not found|is not recognized)\b/.test(value)) return "command_not_found";
|
|
2033
|
+
if (/\b(ts\d{3,4}|typecheck|type error)\b/.test(value)) return "typecheck_error";
|
|
2034
|
+
if (/\b(test failed|assertionerror|not equal)\b/.test(value)) return "test_failure";
|
|
2035
|
+
if (/\b(timed out|timeout|deadline)\b/.test(value)) return "timeout";
|
|
2036
|
+
if (/\b(aborted|cancelled|canceled|sigint|sigterm)\b/.test(value)) return "cancelled";
|
|
2037
|
+
if (/\b(econnrefused|enotfound|fetch failed|network)\b/.test(value)) return "network";
|
|
2038
|
+
if (/\b(invalid json|unexpected token|parse error)\b/.test(value)) return "parse_error";
|
|
2039
|
+
if (/\b(invalid_request_error|missing_tool_result|unresumable)\b/.test(value)) return "unresumable_session";
|
|
2040
|
+
if (/\b(quota reached|quota exceeded|rate limit(?:ed)?|too many requests|resource exhausted|http 429)\b/.test(value)) return "quota";
|
|
2041
|
+
if (/\b(not visible|only the assigned coordinator)\b/.test(value)) return "workflow_scope";
|
|
2042
|
+
return void 0;
|
|
2043
|
+
}
|
|
2044
|
+
function classifyFailure(input) {
|
|
2045
|
+
const tool = input.toolName?.trim() || "unknown";
|
|
2046
|
+
const operation = operationForTool(input.toolName);
|
|
2047
|
+
const fromCode = input.code ? CODE_TO_CLASS[input.code] : void 0;
|
|
2048
|
+
const fromTokens = input.message ? classFromTokens(input.message) : void 0;
|
|
2049
|
+
const diagnosticClass = fromCode ?? fromTokens ?? "unknown";
|
|
2050
|
+
const messageCoordinator = input.message?.match(/assignedCoordinator=([A-Za-z0-9_.-]{1,64})/)?.[1];
|
|
2051
|
+
const messageAction = input.message?.match(/nextAction=([a-z_]{1,64})/)?.[1];
|
|
2052
|
+
const parsedAction = NEXT_ACTIONS.includes(messageAction) ? messageAction : void 0;
|
|
2053
|
+
const nextAction = input.nextAction ?? parsedAction ?? nextActionForCode(input.code, operation);
|
|
2054
|
+
const assignedCoordinatorName = input.assignedCoordinatorName ?? messageCoordinator;
|
|
2055
|
+
return {
|
|
2056
|
+
class: diagnosticClass,
|
|
2057
|
+
tool,
|
|
2058
|
+
operation,
|
|
2059
|
+
...input.statusCode !== void 0 ? { httpStatus: input.statusCode } : {},
|
|
2060
|
+
...input.code ? { code: input.code } : {},
|
|
2061
|
+
...assignedCoordinatorName ? { assignedCoordinatorName } : {},
|
|
2062
|
+
...nextAction ? { nextAction } : {},
|
|
2063
|
+
...input.exitCode !== void 0 ? { exitCode: input.exitCode } : {},
|
|
2064
|
+
...input.durationMs !== void 0 ? { durationMs: input.durationMs } : {}
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
function diagnosticEvidence(diagnostic, toolCallId) {
|
|
2068
|
+
return [
|
|
2069
|
+
`tool:${diagnostic.tool}`,
|
|
2070
|
+
...toolCallId ? [`tool-call:${toolCallId}`] : [],
|
|
2071
|
+
`class:${diagnostic.class}`,
|
|
2072
|
+
`operation:${diagnostic.operation}`,
|
|
2073
|
+
...diagnostic.code ? [`code:${diagnostic.code}`] : [],
|
|
2074
|
+
...diagnostic.nextAction ? [`nextAction:${diagnostic.nextAction}`] : [],
|
|
2075
|
+
...diagnostic.assignedCoordinatorName ? [`assignedCoordinator:${diagnostic.assignedCoordinatorName}`] : []
|
|
2076
|
+
];
|
|
2077
|
+
}
|
|
2078
|
+
function diagnosticSummary(diagnostic) {
|
|
2079
|
+
const coordinator = diagnostic.assignedCoordinatorName ? `; assigned coordinator: ${diagnostic.assignedCoordinatorName}` : "";
|
|
2080
|
+
const next = diagnostic.nextAction ? `; next action: ${diagnostic.nextAction}` : "";
|
|
2081
|
+
return `Tool ${diagnostic.tool} failed: ${diagnostic.class}${coordinator}${next}`;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// plugins/kxm/src/recovery.ts
|
|
2085
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2086
|
+
import { lstatSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
2087
|
+
import { join } from "node:path";
|
|
2088
|
+
function workerStateKey(project, agentName) {
|
|
2089
|
+
const safeProject = project.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 24) || "project";
|
|
2090
|
+
const safeName = agentName.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 32) || "agent";
|
|
2091
|
+
const digest = createHash3("sha256").update(JSON.stringify({ project, agentName })).digest("hex").slice(0, 24);
|
|
2092
|
+
return `${safeProject}-${safeName}-${digest}`;
|
|
2093
|
+
}
|
|
2094
|
+
function legacyRecoveryEnvelopePath(stateDir, agentName) {
|
|
2095
|
+
const safeName = agentName.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
2096
|
+
return join(stateDir, `worker-recovery-${safeName}.json`);
|
|
2097
|
+
}
|
|
2098
|
+
function recoveryEnvelopePath(stateDir, agentName, project) {
|
|
2099
|
+
return project ? join(stateDir, `worker-recovery-${workerStateKey(project, agentName)}.json`) : legacyRecoveryEnvelopePath(stateDir, agentName);
|
|
2100
|
+
}
|
|
2101
|
+
var recoveryReasons = /* @__PURE__ */ new Set([
|
|
2102
|
+
"missing_tool_result",
|
|
2103
|
+
"unresumable_session",
|
|
2104
|
+
"provider_error",
|
|
2105
|
+
"tool_timeout",
|
|
2106
|
+
"worker_signal"
|
|
2107
|
+
]);
|
|
2108
|
+
var recoveryRunId = /^run_[a-f0-9]{32}$/;
|
|
2109
|
+
var recoveryMessageId = /^msg_[a-f0-9]{32}$/;
|
|
2110
|
+
function validBoundedStrings(value, maxItems, pattern) {
|
|
2111
|
+
return Array.isArray(value) && value.length <= maxItems && value.every((item) => typeof item === "string" && item.length > 0 && item.length <= 2048 && !/[\0\r\n]/.test(item) && (!pattern || pattern.test(item)));
|
|
2112
|
+
}
|
|
2113
|
+
function parseRecoveryEnvelope(value, agentName, project) {
|
|
2114
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("recovery envelope must be an object");
|
|
2115
|
+
const parsed = value;
|
|
2116
|
+
if (parsed.version !== 1 || !recoveryReasons.has(parsed.reason) || parsed.agentName !== agentName || typeof parsed.project !== "string" || parsed.project.length < 1 || parsed.project.length > 128 || project !== void 0 && parsed.project !== project || typeof parsed.previousContinue !== "boolean" || typeof parsed.freshSession !== "boolean" || typeof parsed.createdAt !== "string" || !Number.isFinite(Date.parse(parsed.createdAt))) throw new Error("recovery envelope identity or schema is invalid");
|
|
2117
|
+
if (parsed.runId !== void 0 && parsed.runId !== null && !recoveryRunId.test(parsed.runId)) {
|
|
2118
|
+
throw new Error("recovery envelope run identity is invalid");
|
|
2119
|
+
}
|
|
2120
|
+
if (parsed.stageId !== void 0 && parsed.stageId !== null && (typeof parsed.stageId !== "string" || !/^[A-Za-z0-9_.-]{1,128}$/.test(parsed.stageId))) {
|
|
2121
|
+
throw new Error("recovery envelope stage identity is invalid");
|
|
2122
|
+
}
|
|
2123
|
+
if (parsed.activeMessageIds !== void 0 && !validBoundedStrings(parsed.activeMessageIds, 3, recoveryMessageId)) {
|
|
2124
|
+
throw new Error("recovery envelope active messages are invalid");
|
|
2125
|
+
}
|
|
2126
|
+
if (parsed.pendingMessageIds !== void 0 && !validBoundedStrings(parsed.pendingMessageIds, 16, recoveryMessageId)) {
|
|
2127
|
+
throw new Error("recovery envelope pending messages are invalid");
|
|
2128
|
+
}
|
|
2129
|
+
if (parsed.artifactPointers !== void 0 && !validBoundedStrings(parsed.artifactPointers, 16)) {
|
|
2130
|
+
throw new Error("recovery envelope artifact pointers are invalid");
|
|
2131
|
+
}
|
|
2132
|
+
if (parsed.failureClass !== void 0 && parsed.failureClass !== "quota" && parsed.failureClass !== "provider_error" && parsed.failureClass !== "timeout") {
|
|
2133
|
+
throw new Error("recovery envelope failure class is invalid");
|
|
2134
|
+
}
|
|
2135
|
+
if (parsed.signal !== void 0 && (typeof parsed.signal !== "string" || parsed.signal.length > 64 || /[\0\r\n]/.test(parsed.signal))) {
|
|
2136
|
+
throw new Error("recovery envelope signal is invalid");
|
|
2137
|
+
}
|
|
2138
|
+
if (parsed.sessionBinding !== void 0) {
|
|
2139
|
+
const binding = parsed.sessionBinding;
|
|
2140
|
+
if (!binding || typeof binding !== "object" || binding.kind === "default" && Object.keys(binding).some((key) => key !== "kind") || binding.kind === "workflow" && !recoveryRunId.test(binding.runId) || binding.kind !== "default" && binding.kind !== "workflow") throw new Error("recovery envelope session binding is invalid");
|
|
2141
|
+
}
|
|
2142
|
+
return parsed;
|
|
2143
|
+
}
|
|
2144
|
+
function findWorkerRecoveryEnvelope(stateDir, agentName, project) {
|
|
2145
|
+
const candidates = project ? [
|
|
2146
|
+
{ path: recoveryEnvelopePath(stateDir, agentName, project), quarantineInvalid: true },
|
|
2147
|
+
{ path: legacyRecoveryEnvelopePath(stateDir, agentName), quarantineInvalid: false }
|
|
2148
|
+
] : [{ path: legacyRecoveryEnvelopePath(stateDir, agentName), quarantineInvalid: false }];
|
|
2149
|
+
for (const candidate of candidates) {
|
|
2150
|
+
try {
|
|
2151
|
+
const stats = lstatSync(candidate.path);
|
|
2152
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.size > 128 * 1024) {
|
|
2153
|
+
throw new Error("recovery envelope is not a bounded regular file");
|
|
2154
|
+
}
|
|
2155
|
+
const envelope = parseRecoveryEnvelope(JSON.parse(readFileSync(candidate.path, "utf8")), agentName, project);
|
|
2156
|
+
return { envelope, path: candidate.path };
|
|
2157
|
+
} catch (error) {
|
|
2158
|
+
if (error?.code === "ENOENT") continue;
|
|
2159
|
+
if (candidate.quarantineInvalid) {
|
|
2160
|
+
try {
|
|
2161
|
+
renameSync(candidate.path, `${candidate.path}.corrupt-${Date.now()}`);
|
|
2162
|
+
} catch {
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return void 0;
|
|
2168
|
+
}
|
|
2169
|
+
async function consumeWorkerRecoveryEnvelope(client, stateDir, agentName, project) {
|
|
2170
|
+
const found = findWorkerRecoveryEnvelope(stateDir, agentName, project);
|
|
2171
|
+
if (!found) return void 0;
|
|
2172
|
+
const { envelope, path } = found;
|
|
2173
|
+
let runId = envelope.runId ?? void 0;
|
|
2174
|
+
let stageId = envelope.stageId ?? void 0;
|
|
2175
|
+
let scopeMismatch = false;
|
|
2176
|
+
if (envelope.sessionBinding) {
|
|
2177
|
+
const boundRunId = envelope.sessionBinding.kind === "workflow" ? envelope.sessionBinding.runId : void 0;
|
|
2178
|
+
if (runId !== boundRunId) {
|
|
2179
|
+
scopeMismatch = true;
|
|
2180
|
+
runId = void 0;
|
|
2181
|
+
stageId = void 0;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
if (!runId) {
|
|
2185
|
+
rmSync(path, { force: true });
|
|
2186
|
+
return scopeMismatch ? { ...envelope, runId: null, stageId: null } : envelope;
|
|
2187
|
+
}
|
|
2188
|
+
const providerRecovery = envelope.reason === "provider_error";
|
|
2189
|
+
const diagnostic = classifyFailure({
|
|
2190
|
+
toolName: providerRecovery ? "provider" : envelope.reason === "tool_timeout" ? "tool" : "kxm_await",
|
|
2191
|
+
...providerRecovery ? { code: envelope.failureClass === "quota" ? "provider_quota" : "provider_error" } : envelope.reason === "tool_timeout" ? {} : { code: "unresumable_session" },
|
|
2192
|
+
message: envelope.reason === "tool_timeout" ? "timed out" : envelope.reason
|
|
2193
|
+
});
|
|
2194
|
+
try {
|
|
2195
|
+
await client.recordWorkflowEntry(runId, {
|
|
2196
|
+
category: "error",
|
|
2197
|
+
area: "harness",
|
|
2198
|
+
severity: "error",
|
|
2199
|
+
summary: `Worker recovered with ${envelope.reason}`,
|
|
2200
|
+
evidence: [
|
|
2201
|
+
...diagnosticEvidence(diagnostic),
|
|
2202
|
+
`recovery:v1`,
|
|
2203
|
+
`reason:${envelope.reason}`,
|
|
2204
|
+
...envelope.sessionBinding?.kind === "workflow" ? [`session-scope:${envelope.sessionBinding.runId}`] : [],
|
|
2205
|
+
...stageId ? [`stage:${stageId}`] : [],
|
|
2206
|
+
...(envelope.pendingMessageIds ?? []).slice(0, 8).map((id) => `message:${id}`),
|
|
2207
|
+
...(envelope.artifactPointers ?? []).slice(0, 16).map((pointer) => pointer.startsWith("artifact:") ? pointer : `artifact:${pointer}`)
|
|
2208
|
+
]
|
|
2209
|
+
});
|
|
2210
|
+
rmSync(path, { force: true });
|
|
2211
|
+
return { ...envelope, runId, ...stageId ? { stageId } : {} };
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
if (error instanceof HubHttpError && error.statusCode === 403 && error.code === "workflow_forbidden") {
|
|
2214
|
+
rmSync(path, { force: true });
|
|
2215
|
+
return { ...envelope, runId, ...stageId ? { stageId } : {}, peerLocal: true };
|
|
2216
|
+
}
|
|
2217
|
+
return void 0;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// plugins/kxm/src/session-work.ts
|
|
2222
|
+
import { spawnSync } from "node:child_process";
|
|
2223
|
+
import { randomUUID } from "node:crypto";
|
|
2224
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2225
|
+
import { join as join5 } from "node:path";
|
|
2226
|
+
|
|
2227
|
+
// plugins/kxm/src/local-snapshot.ts
|
|
2228
|
+
import { existsSync, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
2229
|
+
import { homedir } from "node:os";
|
|
2230
|
+
import { isAbsolute, join as join2, resolve } from "node:path";
|
|
2231
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2232
|
+
|
|
2233
|
+
// plugins/kxm/src/telemetry.ts
|
|
2234
|
+
import { appendFileSync, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
2235
|
+
function readRoutingRecords(path) {
|
|
2236
|
+
const records = [];
|
|
2237
|
+
try {
|
|
2238
|
+
const raw = readFileSync2(path, "utf8");
|
|
2239
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
2240
|
+
if (!line.trim()) continue;
|
|
2241
|
+
try {
|
|
2242
|
+
const parsed = JSON.parse(line);
|
|
2243
|
+
let routingObj;
|
|
2244
|
+
const recordedAt = typeof parsed.recordedAt === "string" ? parsed.recordedAt : typeof parsed.timestamp === "string" ? parsed.timestamp : (/* @__PURE__ */ new Date()).toISOString();
|
|
2245
|
+
if (parsed.schema === "kxm.routing-record.v2" || parsed.schema === "kxm.routing-record.v1") {
|
|
2246
|
+
routingObj = parsed;
|
|
2247
|
+
} else if (parsed.routing && typeof parsed.routing === "object") {
|
|
2248
|
+
routingObj = parsed.routing;
|
|
2249
|
+
} else if (parsed.envelope && typeof parsed.envelope === "object" && parsed.envelope.routing) {
|
|
2250
|
+
routingObj = parsed.envelope.routing;
|
|
2251
|
+
} else if (parsed.eventType === "routing.attempt.recorded" && parsed.payload && typeof parsed.payload === "object") {
|
|
2252
|
+
routingObj = parsed.payload.routing;
|
|
2253
|
+
}
|
|
2254
|
+
if (routingObj && typeof routingObj === "object") {
|
|
2255
|
+
const r = routingObj;
|
|
2256
|
+
if (r.schema === "kxm.routing-record.v2" || r.schema === "kxm.routing-record.v1") {
|
|
2257
|
+
records.push({ recordedAt, routing: r });
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
} catch {
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
} catch {
|
|
2264
|
+
}
|
|
2265
|
+
return records;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// plugins/kxm/src/local-snapshot.ts
|
|
2269
|
+
function processExists(pid) {
|
|
2270
|
+
try {
|
|
2271
|
+
process.kill(pid, 0);
|
|
2272
|
+
return true;
|
|
2273
|
+
} catch (error) {
|
|
2274
|
+
return error.code === "EPERM";
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
function readJsonRows(database, sql) {
|
|
2278
|
+
const rows = database.prepare(sql).all();
|
|
2279
|
+
const out = [];
|
|
2280
|
+
for (const row of rows) {
|
|
2281
|
+
try {
|
|
2282
|
+
out.push(JSON.parse(row.record));
|
|
2283
|
+
} catch {
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
return out;
|
|
2287
|
+
}
|
|
2288
|
+
var DONE_STAGE_STATUSES = /* @__PURE__ */ new Set(["passed", "failed", "warning"]);
|
|
2289
|
+
function summarizeMeshRun(run) {
|
|
2290
|
+
const stages = (run.stages ?? []).map((stage) => ({
|
|
2291
|
+
id: stage.id,
|
|
2292
|
+
...stage.label ? { label: stage.label } : {},
|
|
2293
|
+
status: stage.status,
|
|
2294
|
+
...stage.attempts ? { attempts: stage.attempts } : {}
|
|
2295
|
+
}));
|
|
2296
|
+
const total = stages.length;
|
|
2297
|
+
const done = stages.filter((stage) => DONE_STAGE_STATUSES.has(stage.status)).length;
|
|
2298
|
+
return {
|
|
2299
|
+
id: run.id,
|
|
2300
|
+
status: run.status,
|
|
2301
|
+
definitionId: run.definitionId,
|
|
2302
|
+
project: run.project,
|
|
2303
|
+
...run.currentStage ? { currentStage: run.currentStage } : {},
|
|
2304
|
+
...run.targetAgentName ? { targetAgentName: run.targetAgentName } : {},
|
|
2305
|
+
...run.updatedAt ? { updatedAt: run.updatedAt } : {},
|
|
2306
|
+
...total > 0 ? { progress: { done, total }, stages } : {}
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
function summarizeMeshPlan(entry) {
|
|
2310
|
+
if (entry.category && entry.category !== "plan") return void 0;
|
|
2311
|
+
const summary = entry.summary.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
2312
|
+
if (!summary) return void 0;
|
|
2313
|
+
return {
|
|
2314
|
+
id: entry.id,
|
|
2315
|
+
runId: entry.runId,
|
|
2316
|
+
summary,
|
|
2317
|
+
createdAt: entry.createdAt,
|
|
2318
|
+
...entry.stageId ? { stageId: entry.stageId } : {},
|
|
2319
|
+
...entry.severity ? { severity: entry.severity } : {}
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
function readPlanMetadata(database) {
|
|
2323
|
+
try {
|
|
2324
|
+
const rows = database.prepare(`
|
|
2325
|
+
SELECT record FROM workflow_journal
|
|
2326
|
+
WHERE category = 'plan'
|
|
2327
|
+
ORDER BY rowid DESC
|
|
2328
|
+
LIMIT 16
|
|
2329
|
+
`).all();
|
|
2330
|
+
const plans = [];
|
|
2331
|
+
for (const row of rows) {
|
|
2332
|
+
try {
|
|
2333
|
+
const parsed = JSON.parse(row.record);
|
|
2334
|
+
if (typeof parsed.id !== "string" || typeof parsed.runId !== "string" || typeof parsed.summary !== "string" || typeof parsed.createdAt !== "string") continue;
|
|
2335
|
+
const plan = summarizeMeshPlan({
|
|
2336
|
+
id: parsed.id,
|
|
2337
|
+
runId: parsed.runId,
|
|
2338
|
+
summary: parsed.summary,
|
|
2339
|
+
createdAt: parsed.createdAt,
|
|
2340
|
+
...parsed.category ? { category: parsed.category } : {},
|
|
2341
|
+
...parsed.stageId ? { stageId: parsed.stageId } : {},
|
|
2342
|
+
...parsed.severity ? { severity: parsed.severity } : {}
|
|
2343
|
+
});
|
|
2344
|
+
if (plan) plans.push(plan);
|
|
2345
|
+
} catch {
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
return plans;
|
|
2349
|
+
} catch {
|
|
2350
|
+
return [];
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
function countRows(database, table, where = "") {
|
|
2354
|
+
const row = database.prepare(`SELECT COUNT(*) AS count FROM ${table}${where}`).get();
|
|
2355
|
+
return Number(row?.count ?? 0);
|
|
2356
|
+
}
|
|
2357
|
+
function readOpenMessageMetadata(database) {
|
|
2358
|
+
const rows = database.prepare(`
|
|
2359
|
+
SELECT
|
|
2360
|
+
json_extract(record, '$.id') AS id,
|
|
2361
|
+
json_extract(record, '$.status') AS status,
|
|
2362
|
+
COALESCE(json_extract(record, '$.fromName'), json_extract(record, '$.from')) AS fromName,
|
|
2363
|
+
COALESCE(json_extract(record, '$.toName'), json_extract(record, '$.to')) AS toName,
|
|
2364
|
+
json_extract(record, '$.delivery') AS delivery,
|
|
2365
|
+
json_extract(record, '$.createdAt') AS createdAt,
|
|
2366
|
+
json_extract(record, '$.correlationId') AS correlationId
|
|
2367
|
+
FROM messages
|
|
2368
|
+
WHERE json_extract(record, '$.status') IN ('queued', 'delivered')
|
|
2369
|
+
ORDER BY json_extract(record, '$.createdAt') DESC
|
|
2370
|
+
LIMIT 16
|
|
2371
|
+
`).all();
|
|
2372
|
+
const messages = [];
|
|
2373
|
+
for (const row of rows) {
|
|
2374
|
+
if (typeof row.id !== "string" || row.status !== "queued" && row.status !== "delivered" || typeof row.fromName !== "string" || typeof row.toName !== "string" || row.delivery !== "steer" && row.delivery !== "followUp" && row.delivery !== "nextTurn" || typeof row.createdAt !== "string") continue;
|
|
2375
|
+
messages.push({
|
|
2376
|
+
id: row.id,
|
|
2377
|
+
status: row.status,
|
|
2378
|
+
fromName: row.fromName,
|
|
2379
|
+
toName: row.toName,
|
|
2380
|
+
delivery: row.delivery,
|
|
2381
|
+
createdAt: row.createdAt,
|
|
2382
|
+
...typeof row.correlationId === "string" ? { correlationId: row.correlationId } : {}
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
return messages;
|
|
2386
|
+
}
|
|
2387
|
+
function resolveKxmSnapshotPaths(cwd, env = process.env) {
|
|
2388
|
+
const stateDir = env.KXM_STATE_DIR?.trim() || join2(cwd, ".kxm", "state");
|
|
2389
|
+
const configured = env.KXM_DATA_PATH?.trim();
|
|
2390
|
+
const dataPath = configured ? resolve(cwd, configured) : join2(stateDir, "kxm.db");
|
|
2391
|
+
return { dataPath, stateDir };
|
|
2392
|
+
}
|
|
2393
|
+
function resolveVnextStateRoot(stateDir, options) {
|
|
2394
|
+
if (options?.vnextStateRoot && existsSync(options.vnextStateRoot)) {
|
|
2395
|
+
return resolve(options.vnextStateRoot);
|
|
2396
|
+
}
|
|
2397
|
+
if (existsSync(join2(stateDir, "runtime", "registry.db")) || existsSync(join2(stateDir, "runtime", "projects"))) {
|
|
2398
|
+
return stateDir;
|
|
2399
|
+
}
|
|
2400
|
+
const env = options?.env ?? process.env;
|
|
2401
|
+
const explicit = env.KXM_STATE_HOME?.trim() || env.KXM_USER_STATE_DIR?.trim() || env.KXM_STATE_ROOT?.trim();
|
|
2402
|
+
if (explicit && isAbsolute(explicit) && existsSync(resolve(explicit))) {
|
|
2403
|
+
return resolve(explicit);
|
|
2404
|
+
}
|
|
2405
|
+
let base;
|
|
2406
|
+
if (process.platform === "win32") {
|
|
2407
|
+
const localAppData = env.LOCALAPPDATA?.trim();
|
|
2408
|
+
base = localAppData && isAbsolute(localAppData) ? localAppData : join2(homedir(), "AppData", "Local");
|
|
2409
|
+
base = resolve(base, "KXM");
|
|
2410
|
+
} else if (process.platform === "darwin") {
|
|
2411
|
+
base = resolve(homedir(), "Library", "Application Support", "KXM");
|
|
2412
|
+
} else {
|
|
2413
|
+
const xdgState = env.XDG_STATE_HOME?.trim();
|
|
2414
|
+
base = xdgState && isAbsolute(xdgState) ? xdgState : join2(homedir(), ".local", "state");
|
|
2415
|
+
base = resolve(base, "kxm");
|
|
2416
|
+
}
|
|
2417
|
+
if (existsSync(base)) return base;
|
|
2418
|
+
return void 0;
|
|
2419
|
+
}
|
|
2420
|
+
function loadLocalMeshSnapshot(dataPath, stateDir, options) {
|
|
2421
|
+
let hasLegacy = false;
|
|
2422
|
+
let agents = [];
|
|
2423
|
+
let openMessages = [];
|
|
2424
|
+
let openMessageTotal = 0;
|
|
2425
|
+
let legacyRuns = [];
|
|
2426
|
+
let legacyRunTotal = 0;
|
|
2427
|
+
let plans = [];
|
|
2428
|
+
if (existsSync(dataPath)) {
|
|
2429
|
+
hasLegacy = true;
|
|
2430
|
+
const database = new DatabaseSync(dataPath, { readOnly: true });
|
|
2431
|
+
try {
|
|
2432
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
2433
|
+
agents = readJsonRows(database, "SELECT record FROM agents");
|
|
2434
|
+
openMessages = readOpenMessageMetadata(database);
|
|
2435
|
+
openMessageTotal = countRows(database, "messages", " WHERE json_extract(record, '$.status') IN ('queued', 'delivered')");
|
|
2436
|
+
legacyRuns = readJsonRows(database, "SELECT record FROM workflow_runs ORDER BY rowid DESC LIMIT 8");
|
|
2437
|
+
legacyRunTotal = countRows(database, "workflow_runs");
|
|
2438
|
+
plans = readPlanMetadata(database);
|
|
2439
|
+
} finally {
|
|
2440
|
+
database.close();
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
let hasVnext = false;
|
|
2444
|
+
const vnextRuns = [];
|
|
2445
|
+
let vnextRunTotal = 0;
|
|
2446
|
+
const vnextStateRoot = resolveVnextStateRoot(stateDir, options);
|
|
2447
|
+
if (vnextStateRoot) {
|
|
2448
|
+
const runtimeDir = join2(vnextStateRoot, "runtime");
|
|
2449
|
+
const registryDbPath = join2(runtimeDir, "registry.db");
|
|
2450
|
+
const projectsDir = join2(runtimeDir, "projects");
|
|
2451
|
+
const projectKeys = /* @__PURE__ */ new Set();
|
|
2452
|
+
if (existsSync(registryDbPath)) {
|
|
2453
|
+
hasVnext = true;
|
|
2454
|
+
try {
|
|
2455
|
+
const regDb = new DatabaseSync(registryDbPath, { readOnly: true });
|
|
2456
|
+
try {
|
|
2457
|
+
regDb.exec("PRAGMA busy_timeout = 5000");
|
|
2458
|
+
const pRows = regDb.prepare("SELECT project_key FROM projects").all();
|
|
2459
|
+
for (const row of pRows) {
|
|
2460
|
+
if (row.project_key) projectKeys.add(row.project_key);
|
|
2461
|
+
}
|
|
2462
|
+
} finally {
|
|
2463
|
+
regDb.close();
|
|
2464
|
+
}
|
|
2465
|
+
} catch {
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
if (existsSync(projectsDir)) {
|
|
2469
|
+
try {
|
|
2470
|
+
for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
2471
|
+
if (entry.isDirectory()) {
|
|
2472
|
+
projectKeys.add(entry.name);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
} catch {
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
for (const key of projectKeys) {
|
|
2479
|
+
const eventDbPath = join2(projectsDir, key, "run-events.db");
|
|
2480
|
+
if (existsSync(eventDbPath)) {
|
|
2481
|
+
hasVnext = true;
|
|
2482
|
+
try {
|
|
2483
|
+
const eventDb = new DatabaseSync(eventDbPath, { readOnly: true });
|
|
2484
|
+
try {
|
|
2485
|
+
eventDb.exec("PRAGMA busy_timeout = 5000");
|
|
2486
|
+
const runRows = eventDb.prepare(`
|
|
2487
|
+
SELECT run_id, project_id, workflow_id, status, created_at, updated_at
|
|
2488
|
+
FROM runs ORDER BY created_at DESC, run_id DESC LIMIT 8
|
|
2489
|
+
`).all();
|
|
2490
|
+
const countRow = eventDb.prepare("SELECT COUNT(*) AS total FROM runs").get();
|
|
2491
|
+
vnextRunTotal += Number(countRow?.total ?? runRows.length);
|
|
2492
|
+
for (const r of runRows) {
|
|
2493
|
+
vnextRuns.push({
|
|
2494
|
+
id: r.run_id,
|
|
2495
|
+
status: r.status,
|
|
2496
|
+
definitionId: r.workflow_id,
|
|
2497
|
+
project: r.project_id,
|
|
2498
|
+
updatedAt: r.updated_at || r.created_at
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
} finally {
|
|
2502
|
+
eventDb.close();
|
|
2503
|
+
}
|
|
2504
|
+
} catch {
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
const pids = [];
|
|
2510
|
+
if (existsSync(stateDir)) {
|
|
2511
|
+
for (const file of readdirSync(stateDir).filter((name) => name.endsWith(".pid"))) {
|
|
2512
|
+
try {
|
|
2513
|
+
const record = JSON.parse(readFileSync3(join2(stateDir, file), "utf8"));
|
|
2514
|
+
pids.push({
|
|
2515
|
+
file,
|
|
2516
|
+
...record.role ? { role: record.role } : {},
|
|
2517
|
+
...record.pid !== void 0 ? { pid: record.pid } : {},
|
|
2518
|
+
live: Number.isInteger(record.pid) && record.pid > 0 && processExists(record.pid)
|
|
2519
|
+
});
|
|
2520
|
+
} catch {
|
|
2521
|
+
pids.push({ file, live: false });
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
const combinedRuns = [
|
|
2526
|
+
...legacyRuns.map((run) => summarizeMeshRun(run)),
|
|
2527
|
+
...vnextRuns
|
|
2528
|
+
];
|
|
2529
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
2530
|
+
const uniqueRuns = [];
|
|
2531
|
+
for (const run of combinedRuns) {
|
|
2532
|
+
if (!seenIds.has(run.id)) {
|
|
2533
|
+
seenIds.add(run.id);
|
|
2534
|
+
uniqueRuns.push(run);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
uniqueRuns.sort((a, b) => {
|
|
2538
|
+
const at = a.updatedAt ? Date.parse(a.updatedAt) : 0;
|
|
2539
|
+
const bt = b.updatedAt ? Date.parse(b.updatedAt) : 0;
|
|
2540
|
+
return bt - at;
|
|
2541
|
+
});
|
|
2542
|
+
const runs = uniqueRuns.slice(0, 16);
|
|
2543
|
+
const runTotal = legacyRunTotal + vnextRunTotal;
|
|
2544
|
+
let source;
|
|
2545
|
+
if (hasLegacy && hasVnext) {
|
|
2546
|
+
source = "both";
|
|
2547
|
+
} else if (hasVnext) {
|
|
2548
|
+
source = "vnext";
|
|
2549
|
+
} else {
|
|
2550
|
+
source = "legacy";
|
|
2551
|
+
}
|
|
2552
|
+
const telemetryFile = join2(stateDir, "telemetry.jsonl");
|
|
2553
|
+
const spend = existsSync(telemetryFile) ? readRoutingRecords(telemetryFile) : [];
|
|
2554
|
+
return {
|
|
2555
|
+
source,
|
|
2556
|
+
agents: agents.sort((a, b) => Number(b.online) - Number(a.online) || a.name.localeCompare(b.name)),
|
|
2557
|
+
openMessages,
|
|
2558
|
+
openMessageTotal,
|
|
2559
|
+
runs,
|
|
2560
|
+
runTotal,
|
|
2561
|
+
plans,
|
|
2562
|
+
pids,
|
|
2563
|
+
spend
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// plugins/kxm/src/kxm-update.ts
|
|
2568
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync, mkdirSync as mkdirSync2 } from "node:fs";
|
|
2569
|
+
import { join as join3 } from "node:path";
|
|
2570
|
+
var KXM_UPDATE_CACHE = "update-check.json";
|
|
2571
|
+
var CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
2572
|
+
function readUpdateCache(stateDir, now = Date.now()) {
|
|
2573
|
+
const path = join3(stateDir, KXM_UPDATE_CACHE);
|
|
2574
|
+
if (!existsSync2(path)) return void 0;
|
|
2575
|
+
try {
|
|
2576
|
+
const row = JSON.parse(readFileSync4(path, "utf8"));
|
|
2577
|
+
if (typeof row.checkedAt !== "number" || !row.notice || now - row.checkedAt > CACHE_TTL_MS) return void 0;
|
|
2578
|
+
if (typeof row.notice.current !== "string" || typeof row.notice.available !== "boolean") return void 0;
|
|
2579
|
+
return row.notice;
|
|
2580
|
+
} catch {
|
|
2581
|
+
return void 0;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
// plugins/kxm/src/hub-binding.ts
|
|
2586
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2587
|
+
import { homedir as homedir2 } from "node:os";
|
|
2588
|
+
import { dirname, isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "node:path";
|
|
2589
|
+
var HUB_BINDING_SCHEMA = "kxm.hub-binding.v1";
|
|
2590
|
+
var HUB_HEALTH_PROBE_MS = 300;
|
|
2591
|
+
var HubBindingError = class extends Error {
|
|
2592
|
+
constructor(message) {
|
|
2593
|
+
super(message);
|
|
2594
|
+
this.name = "HubBindingError";
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
function resolveUserStateRoot(env) {
|
|
2598
|
+
const explicit = env.KXM_STATE_HOME?.trim();
|
|
2599
|
+
if (explicit) {
|
|
2600
|
+
if (!isAbsolute2(explicit)) throw new HubBindingError("local_state_root_not_absolute");
|
|
2601
|
+
return resolve2(explicit);
|
|
2602
|
+
}
|
|
2603
|
+
if (process.platform === "win32") {
|
|
2604
|
+
const localAppData = env.LOCALAPPDATA?.trim();
|
|
2605
|
+
const base2 = localAppData && isAbsolute2(localAppData) ? localAppData : join4(homedir2(), "AppData", "Local");
|
|
2606
|
+
return resolve2(base2, "KXM");
|
|
2607
|
+
}
|
|
2608
|
+
if (process.platform === "darwin") return resolve2(homedir2(), "Library", "Application Support", "KXM");
|
|
2609
|
+
const xdgState = env.XDG_STATE_HOME?.trim();
|
|
2610
|
+
const base = xdgState && isAbsolute2(xdgState) ? xdgState : join4(homedir2(), ".local", "state");
|
|
2611
|
+
return resolve2(base, "kxm");
|
|
2612
|
+
}
|
|
2613
|
+
function hubBindingFile(env = process.env) {
|
|
2614
|
+
return join4(resolveUserStateRoot(env), "hub-binding.json");
|
|
2615
|
+
}
|
|
2616
|
+
function validateHubUrl(raw) {
|
|
2617
|
+
let parsed;
|
|
2618
|
+
try {
|
|
2619
|
+
parsed = new URL(raw);
|
|
2620
|
+
} catch {
|
|
2621
|
+
throw new HubBindingError("hub_url_invalid");
|
|
2622
|
+
}
|
|
2623
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "" || raw.includes("?") || raw.includes("#")) {
|
|
2624
|
+
throw new HubBindingError("hub_url_invalid");
|
|
2625
|
+
}
|
|
2626
|
+
return parsed.href.replace(/\/$/, "");
|
|
2627
|
+
}
|
|
2628
|
+
function isIsoTimestamp(value) {
|
|
2629
|
+
if (Number.isNaN(Date.parse(value))) return false;
|
|
2630
|
+
return value === new Date(value).toISOString();
|
|
2631
|
+
}
|
|
2632
|
+
function isAbortError2(error) {
|
|
2633
|
+
return Boolean(
|
|
2634
|
+
error && typeof error === "object" && ("name" in error && error.name === "AbortError" || "code" in error && error.code === "ABORT_ERR")
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
function readHubBinding(env = process.env) {
|
|
2638
|
+
const file = hubBindingFile(env);
|
|
2639
|
+
if (!existsSync3(file)) return void 0;
|
|
2640
|
+
let parsed;
|
|
2641
|
+
try {
|
|
2642
|
+
parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
2643
|
+
} catch {
|
|
2644
|
+
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
2645
|
+
}
|
|
2646
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2647
|
+
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
2648
|
+
}
|
|
2649
|
+
const row = parsed;
|
|
2650
|
+
const keys = Object.keys(row);
|
|
2651
|
+
if (keys.length !== 3 || row.schema !== HUB_BINDING_SCHEMA || typeof row.url !== "string" || typeof row.boundAt !== "string" || !isIsoTimestamp(row.boundAt)) {
|
|
2652
|
+
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
2653
|
+
}
|
|
2654
|
+
let url;
|
|
2655
|
+
try {
|
|
2656
|
+
url = validateHubUrl(row.url);
|
|
2657
|
+
} catch {
|
|
2658
|
+
throw new HubBindingError(`malformed hub binding at ${file}`);
|
|
2659
|
+
}
|
|
2660
|
+
return { schema: HUB_BINDING_SCHEMA, url, boundAt: row.boundAt };
|
|
2661
|
+
}
|
|
2662
|
+
async function probeHubHealth(url, fetchImpl, timeoutMs = HUB_HEALTH_PROBE_MS) {
|
|
2663
|
+
const started = Date.now();
|
|
2664
|
+
const controller = new AbortController();
|
|
2665
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2666
|
+
try {
|
|
2667
|
+
const response = await fetchImpl(`${url}/health`, { signal: controller.signal });
|
|
2668
|
+
if (!response.ok) return { health: "unknown", probeMs: Date.now() - started };
|
|
2669
|
+
let body;
|
|
2670
|
+
try {
|
|
2671
|
+
body = await response.json();
|
|
2672
|
+
} catch {
|
|
2673
|
+
return { health: "unknown", probeMs: Date.now() - started };
|
|
2674
|
+
}
|
|
2675
|
+
if (body && typeof body === "object" && body.ok === true) {
|
|
2676
|
+
return { health: "on", probeMs: Date.now() - started };
|
|
2677
|
+
}
|
|
2678
|
+
return { health: "unknown", probeMs: Date.now() - started };
|
|
2679
|
+
} catch (error) {
|
|
2680
|
+
return { health: isAbortError2(error) ? "unknown" : "off", probeMs: Date.now() - started };
|
|
2681
|
+
} finally {
|
|
2682
|
+
clearTimeout(timer);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// plugins/kxm/src/session-work.ts
|
|
2687
|
+
var SESSION_BRIEF_SKIP_LABEL = "Skip \u2014 start a fresh session";
|
|
2688
|
+
var MAX_SESSION_BRIEF_TASKS = 5;
|
|
2689
|
+
var MAX_SESSION_BRIEF_PLANS = 5;
|
|
2690
|
+
var SESSION_BRIEF_SCHEMA = "kxm.session-brief.v1";
|
|
2691
|
+
var DEFAULT_SESSION_BRIEF_STALE_SECONDS = 5;
|
|
2692
|
+
function hubPrefix(hub) {
|
|
2693
|
+
if (hub?.state === "on" || hub?.state === void 0 && hub?.online === true) return "kxm hub:on";
|
|
2694
|
+
if (hub?.state === "off" || hub?.state === void 0 && hub?.online === false) return "kxm hub:off";
|
|
2695
|
+
if (hub?.state === "unknown") return "kxm hub:unknown";
|
|
2696
|
+
return "kxm";
|
|
2697
|
+
}
|
|
2698
|
+
function truncate(value, width) {
|
|
2699
|
+
const text = value.replace(/\s+/g, " ").trim();
|
|
2700
|
+
if (text.length <= width) return text;
|
|
2701
|
+
return `${text.slice(0, Math.max(0, width - 1))}\u2026`;
|
|
2702
|
+
}
|
|
2703
|
+
function uniqueLabels(items) {
|
|
2704
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2705
|
+
return items.map((item) => {
|
|
2706
|
+
let label = item.label;
|
|
2707
|
+
if (seen.has(label)) label = truncate(`${label} \xB7 ${item.id.slice(-6)}`, 72);
|
|
2708
|
+
seen.add(label);
|
|
2709
|
+
return label === item.label ? item : { ...item, label };
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2712
|
+
function taskItem(run) {
|
|
2713
|
+
const stage = run.currentStage ?? "-";
|
|
2714
|
+
const progress = run.progress ? ` ${run.progress.done}/${run.progress.total}` : "";
|
|
2715
|
+
const detail = `${run.definitionId}/${stage}${progress}`.trim();
|
|
2716
|
+
const label = truncate(`Task ${run.definitionId} ${run.status} ${stage}${progress}`, 72);
|
|
2717
|
+
const prompt = `Continue KXM task \`${run.definitionId}\` (run ${run.id}) at stage ${stage}${run.progress ? ` (${run.progress.done}/${run.progress.total})` : ""}. Load the run, then proceed without repeating completed work.`;
|
|
2718
|
+
return { kind: "task", id: run.id, runId: run.id, label, detail, prompt };
|
|
2719
|
+
}
|
|
2720
|
+
function planItem(plan) {
|
|
2721
|
+
const label = truncate(`Plan ${plan.summary}`, 72);
|
|
2722
|
+
const prompt = `Continue from KXM plan: ${plan.summary} (run ${plan.runId}). Load that run and proceed.`;
|
|
2723
|
+
return { kind: "plan", id: plan.id, runId: plan.runId, label, detail: plan.summary, prompt };
|
|
2724
|
+
}
|
|
2725
|
+
function recentTasks(runs) {
|
|
2726
|
+
const active = runs.filter(
|
|
2727
|
+
(run) => run.status === "running" || run.status === "waiting" || run.status === "created" || run.status === "preparing"
|
|
2728
|
+
);
|
|
2729
|
+
const rest = runs.filter(
|
|
2730
|
+
(run) => run.status !== "running" && run.status !== "waiting" && run.status !== "created" && run.status !== "preparing"
|
|
2731
|
+
);
|
|
2732
|
+
return [...active, ...rest].slice(0, MAX_SESSION_BRIEF_TASKS);
|
|
2733
|
+
}
|
|
2734
|
+
function formatShipLine(ship) {
|
|
2735
|
+
if (!ship) return "ship verify \xB7 PR=CI";
|
|
2736
|
+
if (ship.dirty) return "ship dirty \xB7 commit after verify";
|
|
2737
|
+
if (ship.ahead > 0) return `ship ${ship.ahead} local \xB7 PR after CI`;
|
|
2738
|
+
return "ship clean \xB7 PR after CI";
|
|
2739
|
+
}
|
|
2740
|
+
function readGitShip(cwd) {
|
|
2741
|
+
try {
|
|
2742
|
+
const dirty = spawnSync("git", ["-C", cwd, "status", "--porcelain"], { encoding: "utf8", windowsHide: true });
|
|
2743
|
+
if (dirty.status !== 0) return void 0;
|
|
2744
|
+
const isDirty = dirty.stdout.trim().length > 0;
|
|
2745
|
+
const upstream = spawnSync("git", ["-C", cwd, "rev-list", "--count", "@{u}..HEAD"], { encoding: "utf8", windowsHide: true });
|
|
2746
|
+
if (upstream.status === 0) {
|
|
2747
|
+
return {
|
|
2748
|
+
dirty: isDirty,
|
|
2749
|
+
ahead: Number.parseInt(upstream.stdout.trim(), 10) || 0
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
for (const baseRef of ["origin/HEAD", "main", "origin/main", "master", "origin/master"]) {
|
|
2753
|
+
const mb = spawnSync("git", ["-C", cwd, "merge-base", baseRef, "HEAD"], { encoding: "utf8", windowsHide: true });
|
|
2754
|
+
if (mb.status === 0 && mb.stdout.trim()) {
|
|
2755
|
+
const count = spawnSync("git", ["-C", cwd, "rev-list", "--count", `${mb.stdout.trim()}..HEAD`], { encoding: "utf8", windowsHide: true });
|
|
2756
|
+
if (count.status === 0) {
|
|
2757
|
+
return {
|
|
2758
|
+
dirty: isDirty,
|
|
2759
|
+
ahead: Number.parseInt(count.stdout.trim(), 10) || 0
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
return {
|
|
2765
|
+
dirty: isDirty,
|
|
2766
|
+
ahead: 0
|
|
2767
|
+
};
|
|
2768
|
+
} catch {
|
|
2769
|
+
return void 0;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
function formatSessionStatusLine(stats, current, hub, ship, updateLatest, cost) {
|
|
2773
|
+
const head = hubPrefix(hub);
|
|
2774
|
+
if (!current && stats.activeTasks === 0 && stats.planCount === 0 && stats.inbox === 0) {
|
|
2775
|
+
const idle = hub?.online === void 0 && hub?.state === void 0 ? "kxm idle" : `${head} \xB7 idle`;
|
|
2776
|
+
const withShip = ship?.dirty ? `${idle} \xB7 dirty` : ship && ship.ahead > 0 ? `${idle} \xB7 ${ship.ahead} local` : idle;
|
|
2777
|
+
const withCost = cost ? `${withShip} \xB7 ${cost}` : withShip;
|
|
2778
|
+
const finalLine = updateLatest ? `${withCost} \xB7 upd ${updateLatest}` : withCost;
|
|
2779
|
+
return finalLine.length <= 80 ? finalLine : `${finalLine.slice(0, 79)}\u2026`;
|
|
2780
|
+
}
|
|
2781
|
+
const parts = [];
|
|
2782
|
+
if (current?.kind === "task") parts.push(`${head} ${current.detail}`);
|
|
2783
|
+
else if (current?.kind === "plan") parts.push(`${head} plan ${truncate(current.detail, 36)}`);
|
|
2784
|
+
else parts.push(head);
|
|
2785
|
+
parts.push(`${stats.activeTasks} task${stats.activeTasks === 1 ? "" : "s"}`);
|
|
2786
|
+
if (stats.waitingTasks > 0) parts.push(`${stats.waitingTasks} waiting`);
|
|
2787
|
+
parts.push(`${stats.planCount} plan${stats.planCount === 1 ? "" : "s"}`);
|
|
2788
|
+
if (stats.inbox > 0) parts.push(`inbox ${stats.inbox}`);
|
|
2789
|
+
if (ship?.dirty) parts.push("dirty");
|
|
2790
|
+
else if (ship && ship.ahead > 0) parts.push(`${ship.ahead} local`);
|
|
2791
|
+
if (cost) parts.push(cost);
|
|
2792
|
+
if (updateLatest) parts.push(`upd ${updateLatest}`);
|
|
2793
|
+
const line = parts.join(" \xB7 ");
|
|
2794
|
+
return line.length <= 80 ? line : `${line.slice(0, 79)}\u2026`;
|
|
2795
|
+
}
|
|
2796
|
+
function formatSessionWidget(stats, current, hub, ship, updateLatest, cost) {
|
|
2797
|
+
const hubMark = hub?.state === "on" || hub?.online === true ? "hub:on " : hub?.state === "off" || hub?.online === false ? "hub:off " : hub?.state === "unknown" ? "hub:unknown " : "";
|
|
2798
|
+
const lines = [
|
|
2799
|
+
`KXM ${hubMark}${stats.activeTasks} tasks ${stats.waitingTasks} waiting ${stats.planCount} plans inbox ${stats.inbox}`
|
|
2800
|
+
];
|
|
2801
|
+
if (current) lines.push(`now ${current.kind} ${truncate(current.detail, 56)}`);
|
|
2802
|
+
else if (stats.latestPlan) lines.push(`plan ${truncate(stats.latestPlan, 60)}`);
|
|
2803
|
+
else lines.push("now no selected work");
|
|
2804
|
+
lines.push(formatShipLine(ship));
|
|
2805
|
+
if (cost) lines.push(`cost ${cost}`);
|
|
2806
|
+
if (updateLatest) lines.push(`update ${updateLatest} available \xB7 kxm update --kxm`);
|
|
2807
|
+
return lines;
|
|
2808
|
+
}
|
|
2809
|
+
function buildSessionBrief(snapshot, current, hub, ship, updateLatest, cost, sessionToken, source, staleSeconds = DEFAULT_SESSION_BRIEF_STALE_SECONDS) {
|
|
2810
|
+
const resolvedSource = source ?? snapshot.source ?? "legacy";
|
|
2811
|
+
const active = snapshot.runs.filter(
|
|
2812
|
+
(run) => run.status === "running" || run.status === "waiting" || run.status === "created" || run.status === "preparing"
|
|
2813
|
+
);
|
|
2814
|
+
const stats = {
|
|
2815
|
+
activeTasks: active.length,
|
|
2816
|
+
waitingTasks: snapshot.runs.filter((run) => run.status === "waiting").length,
|
|
2817
|
+
planCount: snapshot.plans.length,
|
|
2818
|
+
inbox: snapshot.openMessageTotal,
|
|
2819
|
+
runTotal: snapshot.runTotal,
|
|
2820
|
+
...snapshot.plans[0]?.summary ? { latestPlan: snapshot.plans[0].summary } : {}
|
|
2821
|
+
};
|
|
2822
|
+
const tasks = uniqueLabels(recentTasks(snapshot.runs).map(taskItem));
|
|
2823
|
+
const plans = uniqueLabels(snapshot.plans.slice(0, MAX_SESSION_BRIEF_PLANS).map(planItem));
|
|
2824
|
+
const resolvedHub = hub ?? { state: "off", evidence: "unconfigured", online: false };
|
|
2825
|
+
const statusLine = formatSessionStatusLine(stats, current, resolvedHub, ship, updateLatest, cost);
|
|
2826
|
+
const widgetLines = formatSessionWidget(stats, current, resolvedHub, ship, updateLatest, cost);
|
|
2827
|
+
return {
|
|
2828
|
+
schema: SESSION_BRIEF_SCHEMA,
|
|
2829
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2830
|
+
staleSeconds,
|
|
2831
|
+
source: resolvedSource,
|
|
2832
|
+
hub: resolvedHub,
|
|
2833
|
+
stats,
|
|
2834
|
+
tasks,
|
|
2835
|
+
plans,
|
|
2836
|
+
statusLine,
|
|
2837
|
+
widgetLines,
|
|
2838
|
+
...cost ? { cost } : {},
|
|
2839
|
+
...sessionToken ? { sessionToken } : {}
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
function readCachedSessionBrief(stateDir) {
|
|
2843
|
+
const file = join5(stateDir, "session-brief.json");
|
|
2844
|
+
try {
|
|
2845
|
+
if (!existsSync4(file)) return void 0;
|
|
2846
|
+
const raw = readFileSync6(file, "utf8");
|
|
2847
|
+
const parsed = JSON.parse(raw);
|
|
2848
|
+
if (parsed && parsed.schema === "kxm.session-brief.v1" && typeof parsed.generatedAt === "string") {
|
|
2849
|
+
const ageMs = Date.now() - Date.parse(parsed.generatedAt);
|
|
2850
|
+
const ttlMs = (parsed.staleSeconds ?? DEFAULT_SESSION_BRIEF_STALE_SECONDS) * 1e3;
|
|
2851
|
+
if (ageMs >= 0 && ageMs < ttlMs) {
|
|
2852
|
+
return parsed;
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
} catch {
|
|
2856
|
+
}
|
|
2857
|
+
return void 0;
|
|
2858
|
+
}
|
|
2859
|
+
function writeCachedSessionBrief(stateDir, brief) {
|
|
2860
|
+
try {
|
|
2861
|
+
mkdirSync4(stateDir, { recursive: true, mode: 448 });
|
|
2862
|
+
const file = join5(stateDir, "session-brief.json");
|
|
2863
|
+
const tmp = `${file}.tmp.${randomUUID().slice(0, 8)}`;
|
|
2864
|
+
writeFileSync3(tmp, JSON.stringify(brief, null, 2), { encoding: "utf8", mode: 384 });
|
|
2865
|
+
renameSync3(tmp, file);
|
|
2866
|
+
} catch {
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
function estimateSessionCost(stateDir) {
|
|
2870
|
+
try {
|
|
2871
|
+
const telemetryFile = join5(stateDir, "telemetry.jsonl");
|
|
2872
|
+
if (!existsSync4(telemetryFile)) return void 0;
|
|
2873
|
+
const records = readRoutingRecords(telemetryFile);
|
|
2874
|
+
if (records.length === 0) return void 0;
|
|
2875
|
+
let totalCost = 0;
|
|
2876
|
+
let hasMetered = false;
|
|
2877
|
+
let latestModel;
|
|
2878
|
+
let latestHarness;
|
|
2879
|
+
for (const { routing } of records) {
|
|
2880
|
+
const r = routing;
|
|
2881
|
+
const costBasis = r.costBasis ?? (typeof r.costUsd === "number" ? "metered" : "unmetered");
|
|
2882
|
+
if (costBasis === "metered" && typeof r.costUsd === "number") {
|
|
2883
|
+
totalCost += r.costUsd;
|
|
2884
|
+
hasMetered = true;
|
|
2885
|
+
}
|
|
2886
|
+
latestModel = r.effectiveModel ?? r.requestedModel ?? latestModel;
|
|
2887
|
+
latestHarness = r.harness ?? latestHarness;
|
|
2888
|
+
}
|
|
2889
|
+
if (!hasMetered) return "unknown";
|
|
2890
|
+
const route = latestHarness && latestModel ? `${latestHarness}/${latestModel}` : latestModel;
|
|
2891
|
+
return `$${totalCost.toFixed(2)} sess${route ? ` \xB7 ${route}` : ""}`;
|
|
2892
|
+
} catch {
|
|
2893
|
+
return void 0;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
async function resolveSessionHubStatus(url, fetchImpl = fetch, timeoutMs = 300) {
|
|
2897
|
+
if (!url || !url.trim()) {
|
|
2898
|
+
return { state: "off", evidence: "unconfigured", online: false };
|
|
2899
|
+
}
|
|
2900
|
+
try {
|
|
2901
|
+
const { health } = await probeHubHealth(url.trim(), fetchImpl, timeoutMs);
|
|
2902
|
+
if (health === "on") {
|
|
2903
|
+
return { state: "on", evidence: "probed", online: true, url: url.trim() };
|
|
2904
|
+
}
|
|
2905
|
+
if (health === "off") {
|
|
2906
|
+
return { state: "off", evidence: "probed", online: false, url: url.trim() };
|
|
2907
|
+
}
|
|
2908
|
+
return { state: "unknown", evidence: "timeout", online: false, url: url.trim() };
|
|
2909
|
+
} catch {
|
|
2910
|
+
return { state: "unknown", evidence: "timeout", online: false, url: url.trim() };
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
function loadSessionBrief(cwd, env = process.env, current, hub, options = {}) {
|
|
2914
|
+
const paths = resolveKxmSnapshotPaths(cwd, env);
|
|
2915
|
+
if (!options.force) {
|
|
2916
|
+
const cached = readCachedSessionBrief(paths.stateDir);
|
|
2917
|
+
if (cached) {
|
|
2918
|
+
if (current || hub || options.cost) {
|
|
2919
|
+
const effectiveHub = hub ?? cached.hub;
|
|
2920
|
+
const effectiveCost = options.cost ?? cached.cost;
|
|
2921
|
+
const statusLine = formatSessionStatusLine(
|
|
2922
|
+
cached.stats,
|
|
2923
|
+
current,
|
|
2924
|
+
effectiveHub,
|
|
2925
|
+
options.ship,
|
|
2926
|
+
options.updateLatest,
|
|
2927
|
+
effectiveCost
|
|
2928
|
+
);
|
|
2929
|
+
const widgetLines = formatSessionWidget(
|
|
2930
|
+
cached.stats,
|
|
2931
|
+
current,
|
|
2932
|
+
effectiveHub,
|
|
2933
|
+
options.ship,
|
|
2934
|
+
options.updateLatest,
|
|
2935
|
+
effectiveCost
|
|
2936
|
+
);
|
|
2937
|
+
return {
|
|
2938
|
+
...cached,
|
|
2939
|
+
hub: effectiveHub,
|
|
2940
|
+
...effectiveCost ? { cost: effectiveCost } : {},
|
|
2941
|
+
statusLine,
|
|
2942
|
+
widgetLines
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
return cached;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
const ship = options.ship ?? readGitShip(cwd);
|
|
2949
|
+
let brief;
|
|
2950
|
+
try {
|
|
2951
|
+
const cachedUpdate = readUpdateCache(paths.stateDir);
|
|
2952
|
+
const updateLatest = options.updateLatest ?? (cachedUpdate?.available ? cachedUpdate.latest : void 0);
|
|
2953
|
+
const cost = options.cost ?? estimateSessionCost(paths.stateDir);
|
|
2954
|
+
const snapshot = loadLocalMeshSnapshot(paths.dataPath, paths.stateDir, { env });
|
|
2955
|
+
brief = buildSessionBrief(
|
|
2956
|
+
snapshot,
|
|
2957
|
+
current,
|
|
2958
|
+
hub,
|
|
2959
|
+
ship,
|
|
2960
|
+
updateLatest,
|
|
2961
|
+
cost,
|
|
2962
|
+
options.sessionToken,
|
|
2963
|
+
snapshot.source
|
|
2964
|
+
);
|
|
2965
|
+
} catch {
|
|
2966
|
+
brief = buildSessionBrief(
|
|
2967
|
+
{ runs: [], plans: [], openMessageTotal: 0, runTotal: 0 },
|
|
2968
|
+
current,
|
|
2969
|
+
hub,
|
|
2970
|
+
ship,
|
|
2971
|
+
options.updateLatest,
|
|
2972
|
+
options.cost,
|
|
2973
|
+
options.sessionToken
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
writeCachedSessionBrief(paths.stateDir, brief);
|
|
2977
|
+
return brief;
|
|
2978
|
+
}
|
|
2979
|
+
async function loadSessionBriefAsync(cwd, env = process.env, current, hub, options = {}) {
|
|
2980
|
+
const paths = resolveKxmSnapshotPaths(cwd, env);
|
|
2981
|
+
if (!options.force) {
|
|
2982
|
+
const cached = readCachedSessionBrief(paths.stateDir);
|
|
2983
|
+
if (cached) {
|
|
2984
|
+
if (current || hub || options.cost) {
|
|
2985
|
+
const effectiveHub = hub ?? cached.hub;
|
|
2986
|
+
const effectiveCost = options.cost ?? cached.cost;
|
|
2987
|
+
const statusLine = formatSessionStatusLine(
|
|
2988
|
+
cached.stats,
|
|
2989
|
+
current,
|
|
2990
|
+
effectiveHub,
|
|
2991
|
+
options.ship,
|
|
2992
|
+
void 0,
|
|
2993
|
+
effectiveCost
|
|
2994
|
+
);
|
|
2995
|
+
const widgetLines = formatSessionWidget(
|
|
2996
|
+
cached.stats,
|
|
2997
|
+
current,
|
|
2998
|
+
effectiveHub,
|
|
2999
|
+
options.ship,
|
|
3000
|
+
void 0,
|
|
3001
|
+
effectiveCost
|
|
3002
|
+
);
|
|
3003
|
+
return {
|
|
3004
|
+
...cached,
|
|
3005
|
+
hub: effectiveHub,
|
|
3006
|
+
...effectiveCost ? { cost: effectiveCost } : {},
|
|
3007
|
+
statusLine,
|
|
3008
|
+
widgetLines
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
return cached;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
let resolvedHub = hub;
|
|
3015
|
+
if (!resolvedHub) {
|
|
3016
|
+
const serverUrl = env.KXM_SERVER_URL?.trim() || readHubBinding(env)?.url;
|
|
3017
|
+
resolvedHub = await resolveSessionHubStatus(serverUrl, options.fetchImpl, 300);
|
|
3018
|
+
}
|
|
3019
|
+
return loadSessionBrief(cwd, env, current, resolvedHub, options);
|
|
3020
|
+
}
|
|
3021
|
+
function sessionBriefChoices(brief) {
|
|
3022
|
+
return [SESSION_BRIEF_SKIP_LABEL, ...brief.tasks.map((item) => item.label), ...brief.plans.map((item) => item.label)];
|
|
3023
|
+
}
|
|
3024
|
+
function itemFromChoice(brief, choice) {
|
|
3025
|
+
if (!choice || choice === SESSION_BRIEF_SKIP_LABEL) return void 0;
|
|
3026
|
+
return [...brief.tasks, ...brief.plans].find((item) => item.label === choice);
|
|
3027
|
+
}
|
|
3028
|
+
function sessionBriefPickerEnabled(input) {
|
|
3029
|
+
if (input.env?.KXM_SESSION_BRIEF?.trim() === "off") return false;
|
|
3030
|
+
if (input.mode !== "tui") return false;
|
|
3031
|
+
const reason = input.reason ?? "startup";
|
|
3032
|
+
return reason === "startup" || reason === "new" || reason === "fork";
|
|
3033
|
+
}
|
|
3034
|
+
var KXM_SLASH_SUBCOMMANDS = ["status", "hub", "memory", "help"];
|
|
3035
|
+
function parseKxmSlashArgs(args) {
|
|
3036
|
+
const raw = String(args ?? "").trim().toLowerCase();
|
|
3037
|
+
if (raw === "" || raw === "brief") return "brief";
|
|
3038
|
+
if (raw === "status" || raw === "hub" || raw === "memory" || raw === "help") return raw;
|
|
3039
|
+
return "help";
|
|
3040
|
+
}
|
|
3041
|
+
function kxmSlashCompletions(prefix) {
|
|
3042
|
+
const p = prefix.trim().toLowerCase();
|
|
3043
|
+
return KXM_SLASH_SUBCOMMANDS.filter((name) => name.startsWith(p)).map((name) => ({ value: name, label: name }));
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// plugins/kxm/src/extension.ts
|
|
3047
|
+
var SETTLEMENT_RETRY_BASE_MS = 250;
|
|
3048
|
+
var SETTLEMENT_RETRY_MAX_MS = 3e4;
|
|
3049
|
+
function boundedPeerReply(reply) {
|
|
3050
|
+
if (reply.length <= MAX_CONTENT_CHARS) return reply;
|
|
3051
|
+
const suffix = `
|
|
3052
|
+
|
|
3053
|
+
[kxm: response truncated from ${reply.length} characters to fit the message limit; the full output may remain in the replying agent's local session or worker log]`;
|
|
3054
|
+
return reply.slice(0, MAX_CONTENT_CHARS - suffix.length) + suffix;
|
|
3055
|
+
}
|
|
3056
|
+
function isTerminalMessage2(message) {
|
|
3057
|
+
return message.status === "replied" || message.status === "cancelled" || message.status === "expired" || message.status === "error";
|
|
3058
|
+
}
|
|
3059
|
+
function isTerminalMessageError2(error) {
|
|
3060
|
+
return error instanceof HubHttpError && (error.statusCode === 409 || error.statusCode === 404 && error.code === "message_not_found");
|
|
3061
|
+
}
|
|
3062
|
+
var WORKFLOW_RUN_ID = /^run_[a-f0-9]{32}$/;
|
|
3063
|
+
var PI_SESSION_ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
3064
|
+
function workflowRunIdForMessage(message) {
|
|
3065
|
+
if (message.workflowRunId !== void 0) {
|
|
3066
|
+
if (!WORKFLOW_RUN_ID.test(message.workflowRunId)) return void 0;
|
|
3067
|
+
if (message.workflowContext?.runId && message.workflowContext.runId !== message.workflowRunId) return void 0;
|
|
3068
|
+
return message.workflowRunId;
|
|
3069
|
+
}
|
|
3070
|
+
if (message.workflowContext?.runId && WORKFLOW_RUN_ID.test(message.workflowContext.runId)) {
|
|
3071
|
+
return message.workflowContext.runId;
|
|
3072
|
+
}
|
|
3073
|
+
if (message.correlationId && WORKFLOW_RUN_ID.test(message.correlationId) && message.from === `workflow:${message.correlationId}`) return message.correlationId;
|
|
3074
|
+
return void 0;
|
|
3075
|
+
}
|
|
3076
|
+
function bindingForMessage(message) {
|
|
3077
|
+
const runId = workflowRunIdForMessage(message);
|
|
3078
|
+
if (message.workflowRunId !== void 0 && !runId) {
|
|
3079
|
+
throw new Error("hub workflow affinity is malformed or conflicts with workflow context");
|
|
3080
|
+
}
|
|
3081
|
+
if (message.workflowContext?.runId && !WORKFLOW_RUN_ID.test(message.workflowContext.runId)) {
|
|
3082
|
+
throw new Error("hub workflow context contains a malformed run identity");
|
|
3083
|
+
}
|
|
3084
|
+
return runId ? { kind: "workflow", runId } : { kind: "default" };
|
|
3085
|
+
}
|
|
3086
|
+
function bindingFromEnvironment() {
|
|
3087
|
+
if (process.env.KXM_WORKER_SESSION_ISOLATION !== "workflow") return { kind: "default" };
|
|
3088
|
+
const scope = process.env.KXM_WORKER_SESSION_SCOPE?.trim();
|
|
3089
|
+
if (scope === "default") return { kind: "default" };
|
|
3090
|
+
if (scope?.startsWith("workflow:")) {
|
|
3091
|
+
const runId = scope.slice("workflow:".length);
|
|
3092
|
+
if (WORKFLOW_RUN_ID.test(runId)) return { kind: "workflow", runId };
|
|
3093
|
+
}
|
|
3094
|
+
throw new Error("KXM_WORKER_SESSION_SCOPE must be default or workflow:<canonical-run-id>");
|
|
3095
|
+
}
|
|
3096
|
+
function sameBinding(left, right) {
|
|
3097
|
+
return left.kind === right.kind && (left.kind === "default" || right.kind === "workflow" && left.runId === right.runId);
|
|
3098
|
+
}
|
|
3099
|
+
function result(value) {
|
|
3100
|
+
return {
|
|
3101
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
3102
|
+
details: value
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
function latestAssistantMessage(messages) {
|
|
3106
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
3107
|
+
const message = messages[index];
|
|
3108
|
+
if (message?.role === "assistant") return messages[index];
|
|
3109
|
+
}
|
|
3110
|
+
return void 0;
|
|
3111
|
+
}
|
|
3112
|
+
function assistantText(messages) {
|
|
3113
|
+
const message = latestAssistantMessage(messages);
|
|
3114
|
+
if (!message) return void 0;
|
|
3115
|
+
if (typeof message.content === "string") return message.content.trim();
|
|
3116
|
+
if (!Array.isArray(message.content)) return void 0;
|
|
3117
|
+
const text = message.content.map((item) => {
|
|
3118
|
+
const part = item;
|
|
3119
|
+
return part.type === "text" ? part.text ?? "" : "";
|
|
3120
|
+
}).join("").trim();
|
|
3121
|
+
return text || void 0;
|
|
3122
|
+
}
|
|
3123
|
+
function assistantFailure(messages) {
|
|
3124
|
+
const message = latestAssistantMessage(messages);
|
|
3125
|
+
if (!message || message.stopReason !== "error" && message.stopReason !== "aborted") return void 0;
|
|
3126
|
+
if (message.stopReason === "aborted") {
|
|
3127
|
+
return classifyFailure({
|
|
3128
|
+
toolName: "provider",
|
|
3129
|
+
message: "cancelled"
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
const failureMessage = typeof message.errorMessage === "string" ? message.errorMessage : "provider error";
|
|
3133
|
+
const quota = /\b(quota reached|quota exceeded|rate limit(?:ed)?|too many requests|resource exhausted|http 429)\b/i.test(failureMessage);
|
|
3134
|
+
return classifyFailure({
|
|
3135
|
+
toolName: "provider",
|
|
3136
|
+
code: quota ? "provider_quota" : "provider_error",
|
|
3137
|
+
message: failureMessage
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
function piMeshExtension(pi) {
|
|
3141
|
+
let client;
|
|
3142
|
+
let nousReport;
|
|
3143
|
+
let pending = [];
|
|
3144
|
+
let activatingInbound;
|
|
3145
|
+
let awaitingActivation;
|
|
3146
|
+
let activeInbound;
|
|
3147
|
+
let activationInProgress = false;
|
|
3148
|
+
let activationPromise;
|
|
3149
|
+
let activationRetryTimer;
|
|
3150
|
+
let activationRetryAttempt = 0;
|
|
3151
|
+
let activationStartTimer;
|
|
3152
|
+
let activationStartMessageId;
|
|
3153
|
+
let requestWorkerRestart;
|
|
3154
|
+
let activeReply;
|
|
3155
|
+
let settlementReply;
|
|
3156
|
+
let activeTurnSettled = false;
|
|
3157
|
+
let settlementInProgress = false;
|
|
3158
|
+
let settlementRetryAttempt = 0;
|
|
3159
|
+
let settlementRetryTimer;
|
|
3160
|
+
let activeFailure;
|
|
3161
|
+
let activeFailureRecorded = false;
|
|
3162
|
+
let shuttingDown = false;
|
|
3163
|
+
let notify;
|
|
3164
|
+
let stateDir = process.env.KXM_STATE_DIR ?? "";
|
|
3165
|
+
let agentName = process.env.KXM_AGENT_NAME ?? "";
|
|
3166
|
+
let projectName = process.env.KXM_PROJECT ?? "";
|
|
3167
|
+
let recoveryStageId;
|
|
3168
|
+
let recoveryArtifacts = [];
|
|
3169
|
+
let currentPiSessionId;
|
|
3170
|
+
let currentSessionBinding = { kind: "default" };
|
|
3171
|
+
function recoveryContextPath() {
|
|
3172
|
+
if (!stateDir || !agentName || !projectName) return void 0;
|
|
3173
|
+
return join6(stateDir, `worker-context-${workerStateKey(projectName, agentName)}.json`);
|
|
3174
|
+
}
|
|
3175
|
+
function sessionRouteRequestPath() {
|
|
3176
|
+
const workerKey = process.env.KXM_WORKER_IDENTITY_KEY?.trim();
|
|
3177
|
+
if (!stateDir || !workerKey) return void 0;
|
|
3178
|
+
return join6(stateDir, `worker-session-request-${workerKey}.json`);
|
|
3179
|
+
}
|
|
3180
|
+
function requestSessionRoute(message, target) {
|
|
3181
|
+
if (process.env.KXM_WORKER_SESSION_ISOLATION !== "workflow" || sameBinding(currentSessionBinding, target)) {
|
|
3182
|
+
return false;
|
|
3183
|
+
}
|
|
3184
|
+
const path = sessionRouteRequestPath();
|
|
3185
|
+
const workerKey = process.env.KXM_WORKER_IDENTITY_KEY?.trim();
|
|
3186
|
+
const generation = process.env.KXM_WORKER_GENERATION?.trim();
|
|
3187
|
+
const childIncarnation = Number(process.env.KXM_WORKER_CHILD_INCARCATION);
|
|
3188
|
+
if (!path || !workerKey || !generation || !Number.isInteger(childIncarnation) || childIncarnation < 1 || !currentPiSessionId || !PI_SESSION_ID.test(currentPiSessionId)) {
|
|
3189
|
+
throw new Error("supervised workflow session routing is missing a valid worker generation, child incarnation, or Pi session identity");
|
|
3190
|
+
}
|
|
3191
|
+
const request = {
|
|
3192
|
+
version: 1,
|
|
3193
|
+
agentName,
|
|
3194
|
+
project: projectName,
|
|
3195
|
+
workerKey,
|
|
3196
|
+
generation,
|
|
3197
|
+
childIncarnation,
|
|
3198
|
+
from: currentSessionBinding,
|
|
3199
|
+
to: target,
|
|
3200
|
+
sourceSessionId: currentPiSessionId,
|
|
3201
|
+
messageId: message.id,
|
|
3202
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3203
|
+
};
|
|
3204
|
+
mkdirSync5(dirname2(path), { recursive: true });
|
|
3205
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
3206
|
+
writeFileSync4(tmp, `${JSON.stringify(request)}
|
|
3207
|
+
`, { encoding: "utf8", mode: 384 });
|
|
3208
|
+
renameSync4(tmp, path);
|
|
3209
|
+
return true;
|
|
3210
|
+
}
|
|
3211
|
+
function removeMatchingLegacyRecoveryContext() {
|
|
3212
|
+
if (!stateDir || !agentName || !projectName) return;
|
|
3213
|
+
const legacy = join6(stateDir, `worker-context-${agentName.replace(/[^A-Za-z0-9_.-]/g, "_")}.json`);
|
|
3214
|
+
if (legacy === recoveryContextPath()) return;
|
|
3215
|
+
try {
|
|
3216
|
+
const candidate = JSON.parse(readFileSync7(legacy, "utf8"));
|
|
3217
|
+
if (candidate.version === 1 && candidate.agentName === agentName && candidate.project === projectName) {
|
|
3218
|
+
rmSync3(legacy, { force: true });
|
|
3219
|
+
}
|
|
3220
|
+
} catch {
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
function persistRecoveryContext() {
|
|
3224
|
+
const path = recoveryContextPath();
|
|
3225
|
+
if (!path) return;
|
|
3226
|
+
const recoveryMessage = [activeInbound, awaitingActivation, activatingInbound].find((message) => message && workflowRunIdForMessage(message));
|
|
3227
|
+
const runId = recoveryMessage ? workflowRunIdForMessage(recoveryMessage) : void 0;
|
|
3228
|
+
const activeMessageIds = [activeInbound?.id, awaitingActivation?.id, activatingInbound?.id].filter((id) => Boolean(id)).slice(0, 3);
|
|
3229
|
+
const pendingMessageIds = [...activeMessageIds, ...pending.map((message) => message.id)].filter((id, index, values) => Boolean(id) && values.indexOf(id) === index).slice(0, 16);
|
|
3230
|
+
if (!runId && pendingMessageIds.length === 0) {
|
|
3231
|
+
rmSync3(path, { force: true });
|
|
3232
|
+
removeMatchingLegacyRecoveryContext();
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
mkdirSync5(dirname2(path), { recursive: true });
|
|
3236
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
3237
|
+
writeFileSync4(tmp, `${JSON.stringify({ version: 1, agentName, project: projectName, runId: runId ?? null, stageId: recoveryStageId ?? null, activeMessageIds, pendingMessageIds, artifactPointers: recoveryArtifacts.slice(0, 16), updatedAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
3238
|
+
`, { encoding: "utf8", mode: 384 });
|
|
3239
|
+
renameSync4(tmp, path);
|
|
3240
|
+
removeMatchingLegacyRecoveryContext();
|
|
3241
|
+
}
|
|
3242
|
+
async function workflowCall(operation) {
|
|
3243
|
+
try {
|
|
3244
|
+
return await operation();
|
|
3245
|
+
} catch (error) {
|
|
3246
|
+
if (!(error instanceof HubHttpError)) throw error;
|
|
3247
|
+
const assigned = typeof error.extras?.assignedCoordinatorName === "string" ? ` assignedCoordinator=${error.extras.assignedCoordinatorName}` : "";
|
|
3248
|
+
const nextAction = typeof error.extras?.nextAction === "string" ? ` nextAction=${error.extras.nextAction}` : "";
|
|
3249
|
+
const operationName = typeof error.extras?.operation === "string" ? ` operation=${error.extras.operation}` : "";
|
|
3250
|
+
throw new Error(`${error.message} [code=${error.code ?? "http_error"}${operationName}${assigned}${nextAction}]`);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
function requireClient() {
|
|
3254
|
+
if (!client?.agent) throw new Error("kxm hub is not connected; check KXM_SERVER_URL and /kxm hub");
|
|
3255
|
+
return client;
|
|
3256
|
+
}
|
|
3257
|
+
let currentWork;
|
|
3258
|
+
async function applySessionChrome(ctx, event, offerPicker) {
|
|
3259
|
+
const cwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
3260
|
+
const hub = client?.agent ? { state: "on", evidence: "process", online: true } : void 0;
|
|
3261
|
+
const brief = await loadSessionBriefAsync(cwd, process.env, currentWork, hub);
|
|
3262
|
+
ctx.ui.setStatus?.("kxm", brief.statusLine);
|
|
3263
|
+
ctx.ui.setWidget?.("kxm-work", brief.widgetLines);
|
|
3264
|
+
if (!offerPicker || !sessionBriefPickerEnabled({
|
|
3265
|
+
env: process.env,
|
|
3266
|
+
...ctx.mode ? { mode: ctx.mode } : {},
|
|
3267
|
+
...event.reason ? { reason: event.reason } : {}
|
|
3268
|
+
})) return;
|
|
3269
|
+
if (brief.tasks.length === 0 && brief.plans.length === 0) return;
|
|
3270
|
+
if (typeof ctx.ui.select !== "function") return;
|
|
3271
|
+
const choice = await ctx.ui.select("Continue KXM work?", sessionBriefChoices(brief));
|
|
3272
|
+
const item = itemFromChoice(brief, choice);
|
|
3273
|
+
if (!item) return;
|
|
3274
|
+
currentWork = item;
|
|
3275
|
+
const selected = await loadSessionBriefAsync(cwd, process.env, item, hub);
|
|
3276
|
+
ctx.ui.setStatus?.("kxm", selected.statusLine);
|
|
3277
|
+
ctx.ui.setWidget?.("kxm-work", selected.widgetLines);
|
|
3278
|
+
ctx.ui.setEditorText?.(item.prompt);
|
|
3279
|
+
}
|
|
3280
|
+
async function showKxmHub(ctx) {
|
|
3281
|
+
const url = (process.env.KXM_SERVER_URL ?? "http://127.0.0.1:7331").replace(/\/$/, "");
|
|
3282
|
+
let health = "unreachable";
|
|
3283
|
+
try {
|
|
3284
|
+
const response = await fetch(`${url}/health`);
|
|
3285
|
+
health = response.ok ? "ok" : `http_${response.status}`;
|
|
3286
|
+
} catch {
|
|
3287
|
+
health = "unreachable";
|
|
3288
|
+
}
|
|
3289
|
+
if (!client?.agent) {
|
|
3290
|
+
ctx.ui.notify(`kxm hub view: health=${health}; no agent connected`, "warning");
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
const peers = await client.listAgents();
|
|
3294
|
+
ctx.ui.notify(`kxm hub view: health=${health}; ${client.agent.name}; ${peers.length} online agent(s)`, "info");
|
|
3295
|
+
}
|
|
3296
|
+
function clearActivationWatchdog(messageId) {
|
|
3297
|
+
if (messageId && activationStartMessageId !== messageId) return;
|
|
3298
|
+
if (activationStartTimer) clearTimeout(activationStartTimer);
|
|
3299
|
+
activationStartTimer = void 0;
|
|
3300
|
+
activationStartMessageId = void 0;
|
|
3301
|
+
}
|
|
3302
|
+
function startActivationWatchdog(messageId) {
|
|
3303
|
+
clearActivationWatchdog();
|
|
3304
|
+
if (!process.env.KXM_WORKER_IDENTITY_KEY) return;
|
|
3305
|
+
const configured = Number(process.env.KXM_WORKER_ACTIVATION_TIMEOUT_MS?.trim() || 6e4);
|
|
3306
|
+
const timeoutMs = Number.isInteger(configured) && configured >= 1e3 && configured <= 6e5 ? configured : 6e4;
|
|
3307
|
+
activationStartMessageId = messageId;
|
|
3308
|
+
activationStartTimer = setTimeout(() => {
|
|
3309
|
+
activationStartTimer = void 0;
|
|
3310
|
+
if (shuttingDown || activeInbound || awaitingActivation?.id !== messageId) return;
|
|
3311
|
+
notify?.(
|
|
3312
|
+
`kxm worker is stuck: delivered message ${messageId} did not start a model turn within ${timeoutMs}ms; requesting supervised restart`,
|
|
3313
|
+
"error"
|
|
3314
|
+
);
|
|
3315
|
+
persistRecoveryContext();
|
|
3316
|
+
requestWorkerRestart?.();
|
|
3317
|
+
}, timeoutMs);
|
|
3318
|
+
activationStartTimer.unref?.();
|
|
3319
|
+
}
|
|
3320
|
+
function enqueue(message, front = false) {
|
|
3321
|
+
if (front) {
|
|
3322
|
+
pending.unshift(message);
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
const priority = (delivery) => delivery === "steer" ? 0 : delivery === "followUp" ? 1 : 2;
|
|
3326
|
+
const messagePriority = priority(message.delivery);
|
|
3327
|
+
const firstLowerPriority = pending.findIndex((candidate) => priority(candidate.delivery) > messagePriority);
|
|
3328
|
+
if (firstLowerPriority < 0) pending.push(message);
|
|
3329
|
+
else pending.splice(firstLowerPriority, 0, message);
|
|
3330
|
+
}
|
|
3331
|
+
function requestActivation() {
|
|
3332
|
+
if (activationPromise || shuttingDown) return;
|
|
3333
|
+
const task = activateNext();
|
|
3334
|
+
activationPromise = task;
|
|
3335
|
+
void task.finally(() => {
|
|
3336
|
+
if (activationPromise === task) activationPromise = void 0;
|
|
3337
|
+
if (!shuttingDown && !activationRetryTimer && !activatingInbound && !awaitingActivation && !activeInbound && pending.length > 0) requestActivation();
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
function scheduleActivationRetry(error) {
|
|
3341
|
+
if (shuttingDown || activationRetryTimer) return;
|
|
3342
|
+
const delayMs = Math.min(
|
|
3343
|
+
SETTLEMENT_RETRY_BASE_MS * 2 ** Math.min(activationRetryAttempt, 7),
|
|
3344
|
+
SETTLEMENT_RETRY_MAX_MS
|
|
3345
|
+
);
|
|
3346
|
+
activationRetryAttempt += 1;
|
|
3347
|
+
notify?.(
|
|
3348
|
+
`kxm could not activate the next hub message; it remains durable and activation will retry in ${delayMs}ms: ${error instanceof Error ? error.message : String(error)}`,
|
|
3349
|
+
"error"
|
|
3350
|
+
);
|
|
3351
|
+
activationRetryTimer = setTimeout(() => {
|
|
3352
|
+
activationRetryTimer = void 0;
|
|
3353
|
+
requestActivation();
|
|
3354
|
+
}, delayMs);
|
|
3355
|
+
activationRetryTimer.unref?.();
|
|
3356
|
+
}
|
|
3357
|
+
async function activateNext() {
|
|
3358
|
+
if (shuttingDown || !client || activationInProgress || activationRetryTimer || activatingInbound || awaitingActivation || activeInbound) return;
|
|
3359
|
+
activationInProgress = true;
|
|
3360
|
+
try {
|
|
3361
|
+
let message = pending.shift();
|
|
3362
|
+
while (message && (isTerminalMessage2(message) || Date.parse(message.expiresAt) <= Date.now())) {
|
|
3363
|
+
message = pending.shift();
|
|
3364
|
+
}
|
|
3365
|
+
if (!message) {
|
|
3366
|
+
persistRecoveryContext();
|
|
3367
|
+
return;
|
|
3368
|
+
}
|
|
3369
|
+
try {
|
|
3370
|
+
const targetBinding = bindingForMessage(message);
|
|
3371
|
+
if (requestSessionRoute(message, targetBinding)) {
|
|
3372
|
+
enqueue(message, true);
|
|
3373
|
+
persistRecoveryContext();
|
|
3374
|
+
shuttingDown = true;
|
|
3375
|
+
queueMicrotask(() => requestWorkerRestart?.());
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3378
|
+
} catch (error) {
|
|
3379
|
+
enqueue(message, true);
|
|
3380
|
+
persistRecoveryContext();
|
|
3381
|
+
scheduleActivationRetry(error);
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
activatingInbound = message;
|
|
3385
|
+
persistRecoveryContext();
|
|
3386
|
+
let acknowledged;
|
|
3387
|
+
try {
|
|
3388
|
+
acknowledged = await client.acknowledge(message.id);
|
|
3389
|
+
} catch (error) {
|
|
3390
|
+
if (isTerminalMessageError2(error)) {
|
|
3391
|
+
activatingInbound = void 0;
|
|
3392
|
+
activationRetryAttempt = 0;
|
|
3393
|
+
persistRecoveryContext();
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
if (activatingInbound?.id === message.id) {
|
|
3397
|
+
activatingInbound = void 0;
|
|
3398
|
+
enqueue(message, true);
|
|
3399
|
+
persistRecoveryContext();
|
|
3400
|
+
}
|
|
3401
|
+
scheduleActivationRetry(error);
|
|
3402
|
+
return;
|
|
3403
|
+
}
|
|
3404
|
+
if (activatingInbound?.id !== message.id || shuttingDown) return;
|
|
3405
|
+
activationRetryAttempt = 0;
|
|
3406
|
+
activatingInbound = void 0;
|
|
3407
|
+
awaitingActivation = acknowledged;
|
|
3408
|
+
persistRecoveryContext();
|
|
3409
|
+
startActivationWatchdog(acknowledged.id);
|
|
3410
|
+
try {
|
|
3411
|
+
pi.sendMessage({
|
|
3412
|
+
customType: "kxm-inbound",
|
|
3413
|
+
content: [
|
|
3414
|
+
`Peer request from ${acknowledged.fromName} (message ${acknowledged.id}):`,
|
|
3415
|
+
"",
|
|
3416
|
+
acknowledged.content,
|
|
3417
|
+
"",
|
|
3418
|
+
"Respond directly to the peer request. Your settled final response will be returned automatically."
|
|
3419
|
+
].join("\n"),
|
|
3420
|
+
display: true,
|
|
3421
|
+
details: { messageId: acknowledged.id, from: acknowledged.fromName }
|
|
3422
|
+
}, {
|
|
3423
|
+
// Pi's nextTurn mode deliberately waits for a future human prompt. An
|
|
3424
|
+
// autonomous worker has no such prompt, so its durable next item is
|
|
3425
|
+
// normalized to an immediately-triggered follow-up turn.
|
|
3426
|
+
triggerTurn: true,
|
|
3427
|
+
deliverAs: acknowledged.delivery === "nextTurn" ? "followUp" : acknowledged.delivery
|
|
3428
|
+
});
|
|
3429
|
+
} catch (error) {
|
|
3430
|
+
clearActivationWatchdog(acknowledged.id);
|
|
3431
|
+
awaitingActivation = void 0;
|
|
3432
|
+
enqueue(acknowledged, true);
|
|
3433
|
+
persistRecoveryContext();
|
|
3434
|
+
scheduleActivationRetry(error);
|
|
3435
|
+
}
|
|
3436
|
+
} finally {
|
|
3437
|
+
activationInProgress = false;
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
async function receive(event) {
|
|
3441
|
+
if (shuttingDown) return;
|
|
3442
|
+
if (event.type === "message") {
|
|
3443
|
+
if (activeInbound?.id === event.message.id || awaitingActivation?.id === event.message.id || activatingInbound?.id === event.message.id) return;
|
|
3444
|
+
if (pending.some((message) => message.id === event.message.id)) {
|
|
3445
|
+
requestActivation();
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
if (isTerminalMessage2(event.message) || Date.parse(event.message.expiresAt) <= Date.now()) return;
|
|
3449
|
+
if (shuttingDown) return;
|
|
3450
|
+
enqueue(event.message);
|
|
3451
|
+
persistRecoveryContext();
|
|
3452
|
+
requestActivation();
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
if (event.type !== "cancelled" && event.type !== "expired" && event.type !== "reply") return;
|
|
3456
|
+
const messageId = event.message.id;
|
|
3457
|
+
const pendingLength = pending.length;
|
|
3458
|
+
pending = pending.filter((message) => message.id !== messageId);
|
|
3459
|
+
let changed = pending.length !== pendingLength;
|
|
3460
|
+
if (activatingInbound?.id === messageId) {
|
|
3461
|
+
activatingInbound = void 0;
|
|
3462
|
+
changed = true;
|
|
3463
|
+
}
|
|
3464
|
+
if (awaitingActivation?.id === messageId) {
|
|
3465
|
+
clearActivationWatchdog(messageId);
|
|
3466
|
+
awaitingActivation = void 0;
|
|
3467
|
+
changed = true;
|
|
3468
|
+
}
|
|
3469
|
+
if (activeInbound?.id === messageId) {
|
|
3470
|
+
activeInbound = event.message;
|
|
3471
|
+
changed = true;
|
|
3472
|
+
}
|
|
3473
|
+
if (!changed) return;
|
|
3474
|
+
persistRecoveryContext();
|
|
3475
|
+
if (activeInbound?.id === messageId && activeTurnSettled && !settlementInProgress) {
|
|
3476
|
+
finishActiveInbound(messageId);
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
requestActivation();
|
|
3480
|
+
}
|
|
3481
|
+
function finishActiveInbound(messageId) {
|
|
3482
|
+
if (activeInbound?.id !== messageId) return;
|
|
3483
|
+
if (settlementRetryTimer) clearTimeout(settlementRetryTimer);
|
|
3484
|
+
settlementRetryTimer = void 0;
|
|
3485
|
+
settlementRetryAttempt = 0;
|
|
3486
|
+
clearActivationWatchdog(messageId);
|
|
3487
|
+
activeInbound = void 0;
|
|
3488
|
+
activeReply = void 0;
|
|
3489
|
+
settlementReply = void 0;
|
|
3490
|
+
activeTurnSettled = false;
|
|
3491
|
+
activeFailure = void 0;
|
|
3492
|
+
activeFailureRecorded = false;
|
|
3493
|
+
recoveryStageId = void 0;
|
|
3494
|
+
recoveryArtifacts = [];
|
|
3495
|
+
persistRecoveryContext();
|
|
3496
|
+
if (!shuttingDown) requestActivation();
|
|
3497
|
+
}
|
|
3498
|
+
function scheduleSettlementRetry(messageId, error) {
|
|
3499
|
+
if (shuttingDown || !client || activeInbound?.id !== messageId || settlementRetryTimer) return;
|
|
3500
|
+
const delayMs = Math.min(
|
|
3501
|
+
SETTLEMENT_RETRY_BASE_MS * 2 ** Math.min(settlementRetryAttempt, 7),
|
|
3502
|
+
SETTLEMENT_RETRY_MAX_MS
|
|
3503
|
+
);
|
|
3504
|
+
settlementRetryAttempt += 1;
|
|
3505
|
+
persistRecoveryContext();
|
|
3506
|
+
notify?.(
|
|
3507
|
+
`kxm could not return reply for ${messageId}; recovery state was retained and settlement will retry in ${delayMs}ms: ${error instanceof Error ? error.message : String(error)}`,
|
|
3508
|
+
"error"
|
|
3509
|
+
);
|
|
3510
|
+
settlementRetryTimer = setTimeout(() => {
|
|
3511
|
+
settlementRetryTimer = void 0;
|
|
3512
|
+
void settleActiveInbound(messageId);
|
|
3513
|
+
}, delayMs);
|
|
3514
|
+
settlementRetryTimer.unref?.();
|
|
3515
|
+
}
|
|
3516
|
+
async function settleActiveInbound(messageId) {
|
|
3517
|
+
if (!activeInbound || activeInbound.id !== messageId || !client || settlementInProgress) return;
|
|
3518
|
+
settlementInProgress = true;
|
|
3519
|
+
const message = activeInbound;
|
|
3520
|
+
const activeClient = client;
|
|
3521
|
+
const reply = settlementReply ?? boundedPeerReply(activeReply ?? "The peer agent completed without a textual response.");
|
|
3522
|
+
try {
|
|
3523
|
+
let current;
|
|
3524
|
+
try {
|
|
3525
|
+
current = await activeClient.getMessage(message.id);
|
|
3526
|
+
} catch (error) {
|
|
3527
|
+
if (isTerminalMessageError2(error)) {
|
|
3528
|
+
finishActiveInbound(message.id);
|
|
3529
|
+
return;
|
|
3530
|
+
}
|
|
3531
|
+
throw error;
|
|
3532
|
+
}
|
|
3533
|
+
if (isTerminalMessage2(current)) {
|
|
3534
|
+
finishActiveInbound(message.id);
|
|
3535
|
+
return;
|
|
3536
|
+
}
|
|
3537
|
+
await activeClient.reply(message.id, reply);
|
|
3538
|
+
finishActiveInbound(message.id);
|
|
3539
|
+
} catch (error) {
|
|
3540
|
+
if (isTerminalMessageError2(error)) {
|
|
3541
|
+
finishActiveInbound(message.id);
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
scheduleSettlementRetry(message.id, error);
|
|
3545
|
+
} finally {
|
|
3546
|
+
settlementInProgress = false;
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
pi.on("session_start", async (event, ctx) => {
|
|
3550
|
+
shuttingDown = false;
|
|
3551
|
+
if (nousReport?.guidance.length) {
|
|
3552
|
+
for (const item of nousReport.guidance) {
|
|
3553
|
+
ctx.ui.notify(item.message, item.level);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
await client?.stop();
|
|
3557
|
+
client = void 0;
|
|
3558
|
+
try {
|
|
3559
|
+
currentSessionBinding = bindingFromEnvironment();
|
|
3560
|
+
} catch (error) {
|
|
3561
|
+
await applySessionChrome(ctx, event, false);
|
|
3562
|
+
ctx.ui.notify(`kxm session routing configuration failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3563
|
+
void ctx.shutdown();
|
|
3564
|
+
return;
|
|
3565
|
+
}
|
|
3566
|
+
currentPiSessionId = ctx.sessionManager?.getSessionId();
|
|
3567
|
+
const serverUrl = process.env.KXM_SERVER_URL ?? "http://127.0.0.1:7331";
|
|
3568
|
+
const project = process.env.KXM_PROJECT ?? basename(ctx.cwd);
|
|
3569
|
+
const name = process.env.KXM_AGENT_NAME ?? pi.getSessionName() ?? `pi-${process.pid}`;
|
|
3570
|
+
agentName = name;
|
|
3571
|
+
projectName = project;
|
|
3572
|
+
stateDir = process.env.KXM_STATE_DIR ?? stateDir;
|
|
3573
|
+
removeMatchingLegacyRecoveryContext();
|
|
3574
|
+
const purpose = process.env.KXM_AGENT_PURPOSE ?? "General-purpose coding agent";
|
|
3575
|
+
const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : void 0;
|
|
3576
|
+
client = new HubClient({
|
|
3577
|
+
serverUrl,
|
|
3578
|
+
name,
|
|
3579
|
+
purpose,
|
|
3580
|
+
project,
|
|
3581
|
+
...process.env.KXM_AUTH_TOKEN ? { authToken: process.env.KXM_AUTH_TOKEN } : {},
|
|
3582
|
+
...model ? { model } : {}
|
|
3583
|
+
});
|
|
3584
|
+
notify = (message, type) => ctx.ui.notify(message, type);
|
|
3585
|
+
requestWorkerRestart = () => {
|
|
3586
|
+
void ctx.shutdown();
|
|
3587
|
+
};
|
|
3588
|
+
try {
|
|
3589
|
+
const agent = await client.start(receive);
|
|
3590
|
+
ctx.ui.notify(`Connected to the KXM hub as ${agent.name}`, "info");
|
|
3591
|
+
const recovered = stateDir ? await consumeWorkerRecoveryEnvelope(client, stateDir, agent.name, project) : void 0;
|
|
3592
|
+
const recoveryReplayIds = recovered?.activeMessageIds ?? recovered?.pendingMessageIds ?? [];
|
|
3593
|
+
const replayCandidates = await Promise.all(recoveryReplayIds.slice(0, 16).map(async (messageId) => {
|
|
3594
|
+
try {
|
|
3595
|
+
const message = await client.getMessage(messageId);
|
|
3596
|
+
return message.status === "queued" || message.status === "delivered";
|
|
3597
|
+
} catch {
|
|
3598
|
+
return false;
|
|
3599
|
+
}
|
|
3600
|
+
}));
|
|
3601
|
+
const durableInboundWillReplay = replayCandidates.some(Boolean);
|
|
3602
|
+
const recoveryMatchesSession = process.env.KXM_WORKER_SESSION_ISOLATION !== "workflow" || currentSessionBinding.kind === "workflow" && recovered?.runId === currentSessionBinding.runId;
|
|
3603
|
+
if (recovered?.freshSession && recovered.runId && !recovered.peerLocal && recoveryMatchesSession && !durableInboundWillReplay) {
|
|
3604
|
+
pi.sendMessage({ customType: "kxm-recovery", content: [`Resume durable workflow run ${recovered.runId} after a fresh-session worker recovery.`, recovered.stageId ? `Last recorded stage: ${recovered.stageId}.` : "Resolve the current stage from kxm_workflow_get.", `Recovery reason: ${recovered.reason}.`, "Call kxm_workflow_get, inspect its journal and stage evidence, then continue the current stage without repeating completed work.", "Record the recovery decision and checkpoint only after the required evidence is satisfied."].join("\n"), display: true, details: { runId: recovered.runId, stageId: recovered.stageId, reason: recovered.reason } }, { triggerTurn: true, deliverAs: "followUp" });
|
|
3605
|
+
await applySessionChrome(ctx, event, false);
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
} catch (error) {
|
|
3609
|
+
client = void 0;
|
|
3610
|
+
ctx.ui.notify(`kxm connection failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3611
|
+
}
|
|
3612
|
+
await applySessionChrome(ctx, event, true);
|
|
3613
|
+
});
|
|
3614
|
+
pi.on("message_start", (event) => {
|
|
3615
|
+
const message = event.message;
|
|
3616
|
+
if (message.customType !== "kxm-inbound" || !awaitingActivation) return;
|
|
3617
|
+
if (message.details?.messageId !== awaitingActivation.id) return;
|
|
3618
|
+
clearActivationWatchdog(awaitingActivation.id);
|
|
3619
|
+
activeInbound = awaitingActivation;
|
|
3620
|
+
awaitingActivation = void 0;
|
|
3621
|
+
activeReply = void 0;
|
|
3622
|
+
settlementReply = void 0;
|
|
3623
|
+
activeTurnSettled = false;
|
|
3624
|
+
activeFailure = void 0;
|
|
3625
|
+
activeFailureRecorded = false;
|
|
3626
|
+
settlementRetryAttempt = 0;
|
|
3627
|
+
recoveryStageId = void 0;
|
|
3628
|
+
recoveryArtifacts = [];
|
|
3629
|
+
persistRecoveryContext();
|
|
3630
|
+
});
|
|
3631
|
+
pi.on("agent_end", (event) => {
|
|
3632
|
+
if (!activeInbound || activeTurnSettled) return;
|
|
3633
|
+
const latest = latestAssistantMessage(event.messages);
|
|
3634
|
+
if (!latest) return;
|
|
3635
|
+
const failure = assistantFailure(event.messages);
|
|
3636
|
+
if (failure) {
|
|
3637
|
+
activeFailure = failure;
|
|
3638
|
+
activeFailureRecorded = false;
|
|
3639
|
+
activeReply = void 0;
|
|
3640
|
+
persistRecoveryContext();
|
|
3641
|
+
return;
|
|
3642
|
+
}
|
|
3643
|
+
activeFailure = void 0;
|
|
3644
|
+
activeFailureRecorded = false;
|
|
3645
|
+
activeReply = assistantText(event.messages) ?? activeReply;
|
|
3646
|
+
});
|
|
3647
|
+
pi.on("tool_result", async (event) => {
|
|
3648
|
+
const activeRunId = activeInbound ? workflowRunIdForMessage(activeInbound) : void 0;
|
|
3649
|
+
if (!activeRunId || !client) return;
|
|
3650
|
+
const resultEvent = event;
|
|
3651
|
+
const serializedDetails = JSON.stringify(resultEvent.details ?? {});
|
|
3652
|
+
const stageMatch = serializedDetails.match(/"(?:currentStage|stageId)"\s*:\s*"([A-Za-z0-9_.-]{1,64})"/);
|
|
3653
|
+
if (stageMatch?.[1]) recoveryStageId = stageMatch[1];
|
|
3654
|
+
recoveryArtifacts = [.../* @__PURE__ */ new Set([...recoveryArtifacts, ...serializedDetails.match(/(?:artifact:|\.kxm[\\/]assets[\\/])[A-Za-z0-9_./\\:-]{1,240}/g) ?? []])].slice(0, 16);
|
|
3655
|
+
persistRecoveryContext();
|
|
3656
|
+
if (!resultEvent.isError) return;
|
|
3657
|
+
const diagnostic = classifyFailure({
|
|
3658
|
+
...resultEvent.toolName ? { toolName: resultEvent.toolName } : {},
|
|
3659
|
+
...resultEvent.error ?? resultEvent.text ? { message: resultEvent.error ?? resultEvent.text } : {},
|
|
3660
|
+
...resultEvent.code ? { code: resultEvent.code } : {},
|
|
3661
|
+
...resultEvent.statusCode !== void 0 ? { statusCode: resultEvent.statusCode } : {}
|
|
3662
|
+
});
|
|
3663
|
+
try {
|
|
3664
|
+
await client.recordWorkflowEntry(activeRunId, {
|
|
3665
|
+
category: "error",
|
|
3666
|
+
area: areaForTool(resultEvent.toolName),
|
|
3667
|
+
severity: "error",
|
|
3668
|
+
summary: diagnosticSummary(diagnostic),
|
|
3669
|
+
evidence: diagnosticEvidence(diagnostic, resultEvent.toolCallId)
|
|
3670
|
+
});
|
|
3671
|
+
} catch {
|
|
3672
|
+
}
|
|
3673
|
+
});
|
|
3674
|
+
pi.on("agent_settled", async () => {
|
|
3675
|
+
if (!activeInbound || !client || settlementInProgress) return;
|
|
3676
|
+
if (activeFailure) {
|
|
3677
|
+
persistRecoveryContext();
|
|
3678
|
+
if (!activeFailureRecorded) {
|
|
3679
|
+
try {
|
|
3680
|
+
const activeRunId = workflowRunIdForMessage(activeInbound);
|
|
3681
|
+
if (activeRunId) {
|
|
3682
|
+
await client.recordWorkflowEntry(activeRunId, {
|
|
3683
|
+
category: "error",
|
|
3684
|
+
area: "harness",
|
|
3685
|
+
severity: "error",
|
|
3686
|
+
summary: diagnosticSummary(activeFailure),
|
|
3687
|
+
evidence: diagnosticEvidence(activeFailure)
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
activeFailureRecorded = true;
|
|
3691
|
+
} catch {
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
notify?.(
|
|
3695
|
+
`kxm retained ${activeInbound.id} after ${activeFailure.class}; switch the model or let the supervised worker recover it`,
|
|
3696
|
+
"error"
|
|
3697
|
+
);
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3700
|
+
if (!activeTurnSettled) {
|
|
3701
|
+
settlementReply = boundedPeerReply(
|
|
3702
|
+
activeReply ?? "The peer agent completed without a textual response."
|
|
3703
|
+
);
|
|
3704
|
+
}
|
|
3705
|
+
activeTurnSettled = true;
|
|
3706
|
+
await settleActiveInbound(activeInbound.id);
|
|
3707
|
+
});
|
|
3708
|
+
pi.on("session_shutdown", async () => {
|
|
3709
|
+
shuttingDown = true;
|
|
3710
|
+
clearActivationWatchdog();
|
|
3711
|
+
persistRecoveryContext();
|
|
3712
|
+
if (activationRetryTimer) clearTimeout(activationRetryTimer);
|
|
3713
|
+
activationRetryTimer = void 0;
|
|
3714
|
+
activationRetryAttempt = 0;
|
|
3715
|
+
await activationPromise?.catch(() => void 0);
|
|
3716
|
+
if (settlementRetryTimer) clearTimeout(settlementRetryTimer);
|
|
3717
|
+
settlementRetryTimer = void 0;
|
|
3718
|
+
await client?.stop();
|
|
3719
|
+
client = void 0;
|
|
3720
|
+
notify = void 0;
|
|
3721
|
+
requestWorkerRestart = void 0;
|
|
3722
|
+
});
|
|
3723
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
3724
|
+
await applySessionChrome(ctx, { reason: "turn_end" }, false);
|
|
3725
|
+
});
|
|
3726
|
+
for (const cmd of AGENT_COMMANDS) {
|
|
3727
|
+
pi.registerTool({
|
|
3728
|
+
name: cmd.name,
|
|
3729
|
+
label: cmd.label,
|
|
3730
|
+
description: cmd.description,
|
|
3731
|
+
parameters: cmd.parameters,
|
|
3732
|
+
async execute(_toolCallId, params, signal) {
|
|
3733
|
+
const policy = enforceToolPolicy(cmd.name);
|
|
3734
|
+
if (!policy.allowed) {
|
|
3735
|
+
throw new Error(`tool_policy_denied: ${policy.detail ?? policy.error}`);
|
|
3736
|
+
}
|
|
3737
|
+
return result(
|
|
3738
|
+
await workflowCall(
|
|
3739
|
+
() => cmd.execute(requireClient(), params ?? {}, { signal })
|
|
3740
|
+
)
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
pi.registerCommand("kxm", {
|
|
3746
|
+
description: "Hub session brief, status line, and hub view",
|
|
3747
|
+
getArgumentCompletions: (prefix) => {
|
|
3748
|
+
const items = kxmSlashCompletions(prefix);
|
|
3749
|
+
return items.length > 0 ? items : null;
|
|
3750
|
+
},
|
|
3751
|
+
handler: async (args, ctx) => {
|
|
3752
|
+
const command = parseKxmSlashArgs(typeof args === "string" ? args : void 0);
|
|
3753
|
+
if (command === "hub") {
|
|
3754
|
+
await showKxmHub(ctx);
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
if (command === "status") {
|
|
3758
|
+
const cwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
3759
|
+
const hub = client?.agent ? { state: "on", evidence: "process", online: true } : void 0;
|
|
3760
|
+
const brief = await loadSessionBriefAsync(cwd, process.env, currentWork, hub);
|
|
3761
|
+
ctx.ui.setStatus?.("kxm", brief.statusLine);
|
|
3762
|
+
ctx.ui.setWidget?.("kxm-work", brief.widgetLines);
|
|
3763
|
+
ctx.ui.notify(brief.statusLine, "info");
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
if (command === "help") {
|
|
3767
|
+
ctx.ui.notify("kxm: /kxm | /kxm status | /kxm hub | /kxm memory | /kxm help. CLI: kxm session brief, kxm hub view, kxm memory brief", "info");
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
if (command === "memory") {
|
|
3771
|
+
const cwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
3772
|
+
const kxmBin = process.env.KXM_BIN || "kxm";
|
|
3773
|
+
const res = spawnSync2(kxmBin, ["memory", "brief"], { cwd, encoding: "utf8", windowsHide: true });
|
|
3774
|
+
let memText = res.status === 0 && res.stdout ? res.stdout.trim() : "";
|
|
3775
|
+
if (!memText) {
|
|
3776
|
+
const script = join6(cwd, "scripts", "kxm.mjs");
|
|
3777
|
+
if (existsSync5(script)) {
|
|
3778
|
+
const scriptRes = spawnSync2(process.execPath, [script, "memory", "brief"], { cwd, encoding: "utf8", windowsHide: true });
|
|
3779
|
+
if (scriptRes.status === 0 && scriptRes.stdout) memText = scriptRes.stdout.trim();
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
ctx.ui.notify(memText || "No active project memory facts.", "info");
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
await applySessionChrome(ctx, { reason: "new" }, command === "brief");
|
|
3786
|
+
}
|
|
3787
|
+
});
|
|
3788
|
+
return nousFactoryWork(pi, (report) => {
|
|
3789
|
+
nousReport = report;
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
export {
|
|
3793
|
+
assistantFailure,
|
|
3794
|
+
bindingForMessage,
|
|
3795
|
+
piMeshExtension as default,
|
|
3796
|
+
workflowRunIdForMessage
|
|
3797
|
+
};
|