threadwire 0.1.13 → 0.1.15
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/CHANGELOG.md +12 -0
- package/README.md +11 -2
- package/docs/capacity-admission.md +147 -0
- package/docs/development-container.md +77 -0
- package/package.json +1 -1
- package/scripts/verify-package.js +8 -0
- package/src/absolute-deadline.js +6 -3
- package/src/activity-log-status.js +291 -0
- package/src/activity-log.js +91 -3
- package/src/cli.js +153 -8
- package/src/docker-api.js +8 -0
- package/src/isolated-runtime-client.js +124 -4
- package/src/isolated-runtime.js +145 -11
- package/src/jsonl-record-spool.js +178 -0
- package/src/kimi-oauth-store.js +16 -0
- package/src/provider-capacity-codex.js +176 -0
- package/src/provider-capacity-kimi.js +105 -0
- package/src/provider-capacity.js +292 -0
- package/src/providers/index.js +110 -6
- package/src/providers/kimi.js +30 -0
- package/src/run-worker.js +286 -71
- package/src/telegram-ingress/core.js +51 -22
package/src/providers/index.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import {buildClaudeCommand, claudeSessionId, createClaudeParser} from "./claude.js"
|
|
4
4
|
import {buildCodexCommand, codexSessionId, parseCodexEvent} from "./codex.js"
|
|
5
|
-
import {buildKimiCommand, createKimiParser, createKimiSessionId} from "./kimi.js"
|
|
5
|
+
import {buildKimiCommand, createKimiCompletion, createKimiParser, createKimiSessionId} from "./kimi.js"
|
|
6
6
|
import {buildOpenCodeCommand, createOpenCodeParser, createOpenCodeSessionId} from "./opencode.js"
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* @typedef {{
|
|
9
|
+
* @typedef {{disposition: "retrying" | "blocked", category: "authentication" | "permission" | "rate-limit" | "quota" | "billing" | "model" | "network" | "protocol" | "unknown", retryAfterMs?: number}} ProviderHealth
|
|
10
|
+
* @typedef {{name: "codex" | "claude" | "kimi" | "opencode", executable: string, arguments: string[], parse: (record: unknown) => import("../types.js").WorkerEvent[], sessionId: (record: unknown) => string | undefined, health: (record: unknown) => ProviderHealth | undefined, completion?: (record: unknown) => boolean}} Provider
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
/** @type {readonly ["codex", "claude", "kimi", "opencode"]} */
|
|
@@ -16,19 +17,122 @@ export const PROVIDERS = ["codex", "claude", "kimi", "opencode"]
|
|
|
16
17
|
export function createProvider(name, providerArguments, prompt, resumeSession, environment = process.env) {
|
|
17
18
|
if (name === "codex") {
|
|
18
19
|
const command = buildCodexCommand(providerArguments, prompt, resumeSession, environment)
|
|
19
|
-
return {...command, name: "codex", parse: parseCodexEvent, sessionId: codexSessionId}
|
|
20
|
+
return {...command, name: "codex", parse: parseCodexEvent, sessionId: codexSessionId, health: extractProviderHealth}
|
|
20
21
|
}
|
|
21
22
|
if (name === "claude") {
|
|
22
23
|
const command = buildClaudeCommand(providerArguments, prompt, resumeSession, environment)
|
|
23
|
-
return {...command, name: "claude", parse: createClaudeParser(), sessionId: claudeSessionId}
|
|
24
|
+
return {...command, name: "claude", parse: createClaudeParser(), sessionId: claudeSessionId, health: extractProviderHealth}
|
|
24
25
|
}
|
|
25
26
|
if (name === "opencode") {
|
|
26
27
|
const command = buildOpenCodeCommand(providerArguments, prompt, resumeSession, environment)
|
|
27
|
-
return {...command, name: "opencode", parse: createOpenCodeParser(), sessionId: createOpenCodeSessionId()}
|
|
28
|
+
return {...command, name: "opencode", parse: createOpenCodeParser(), sessionId: createOpenCodeSessionId(), health: extractProviderHealth}
|
|
28
29
|
}
|
|
29
30
|
if (name === "kimi") {
|
|
30
31
|
const command = buildKimiCommand(providerArguments, prompt, resumeSession, environment)
|
|
31
|
-
return {...command, name: "kimi", parse: createKimiParser(), sessionId: createKimiSessionId()}
|
|
32
|
+
return {...command, name: "kimi", parse: createKimiParser(), sessionId: createKimiSessionId(), completion: createKimiCompletion(), health: extractProviderHealth}
|
|
32
33
|
}
|
|
33
34
|
throw new Error(`--provider must be one of: ${PROVIDERS.join(", ")}`)
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract safe provider health information from a structured provider record.
|
|
39
|
+
* Inspects only an explicit small list of safe containers — the top-level
|
|
40
|
+
* record and known nested `error`, `part`, `part.error`, or `*.data` objects —
|
|
41
|
+
* never traverses arbitrarily nested objects and never regexes raw message
|
|
42
|
+
* text. Generic HTTP 429 → retrying rate-limit. Structured
|
|
43
|
+
* exceeded_current_quota_error / insufficient_quota / insufficient_balance →
|
|
44
|
+
* blocked quota/billing. If no structured fields are exposed — common when a
|
|
45
|
+
* provider protocol suppresses upstream error details — no health fact is
|
|
46
|
+
* written and the run status remains running/unknown; Hermes must perform an
|
|
47
|
+
* independent bounded provider probe.
|
|
48
|
+
* @param {unknown} record
|
|
49
|
+
* @returns {ProviderHealth | undefined}
|
|
50
|
+
*/
|
|
51
|
+
export function extractProviderHealth(record) {
|
|
52
|
+
if (!isRecord(record)) return undefined
|
|
53
|
+
|
|
54
|
+
// Explicit containers to inspect: top-level record, record.error, record.part,
|
|
55
|
+
// record.part.error. Only these bounded paths; never recursive traversal.
|
|
56
|
+
const containers = [record]
|
|
57
|
+
if (isRecord(record.error)) containers.push(record.error)
|
|
58
|
+
if (isRecord(record.part)) {
|
|
59
|
+
containers.push(record.part)
|
|
60
|
+
if (isRecord(record.part.error)) containers.push(record.part.error)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const container of containers) {
|
|
64
|
+
const health = extractFromContainer(container)
|
|
65
|
+
if (health !== undefined) return health
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Inspect one safe container for structured health fields.
|
|
73
|
+
* @param {Record<string, unknown>} container
|
|
74
|
+
* @returns {ProviderHealth | undefined}
|
|
75
|
+
*/
|
|
76
|
+
function extractFromContainer(container) {
|
|
77
|
+
// Inspect structured data sub-object when present.
|
|
78
|
+
if (isRecord(container.data)) return extractFromContainer(container.data)
|
|
79
|
+
|
|
80
|
+
const httpStatus = container.http_status ?? container.httpStatus ?? container.status_code ?? container.statusCode
|
|
81
|
+
if (typeof httpStatus === "number" && Number.isSafeInteger(httpStatus)) {
|
|
82
|
+
if (httpStatus === 429) {
|
|
83
|
+
const retryAfterMs = safeRetryMs(container.retryAfterMs ?? container.retry_after_ms)
|
|
84
|
+
return {disposition: "retrying", category: "rate-limit", ...(retryAfterMs === undefined ? {} : {retryAfterMs})}
|
|
85
|
+
}
|
|
86
|
+
if (httpStatus >= 500 && httpStatus < 600) {
|
|
87
|
+
return {disposition: "retrying", category: "unknown"}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const code = container.code ?? container.error_code ?? container.errorCode ?? container.error_type ?? container.errorType
|
|
92
|
+
if (typeof code === "string") {
|
|
93
|
+
if (code === "exceeded_current_quota_error" || code === "insufficient_quota" || code === "quota_exceeded") {
|
|
94
|
+
return {disposition: "blocked", category: "quota"}
|
|
95
|
+
}
|
|
96
|
+
if (code === "insufficient_balance" || code === "billing_error" || code === "payment_required") {
|
|
97
|
+
return {disposition: "blocked", category: "billing"}
|
|
98
|
+
}
|
|
99
|
+
if (code === "invalid_api_key" || code === "unauthorized" || code === "auth_error" || code === "authentication_error") {
|
|
100
|
+
return {disposition: "blocked", category: "authentication"}
|
|
101
|
+
}
|
|
102
|
+
if (code === "forbidden" || code === "permission_denied") {
|
|
103
|
+
return {disposition: "blocked", category: "permission"}
|
|
104
|
+
}
|
|
105
|
+
if (code === "model_not_found" || code === "invalid_model" || code === "model_unavailable") {
|
|
106
|
+
return {disposition: "blocked", category: "model"}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (container.insufficient_quota === true || container.quota_exceeded === true) {
|
|
111
|
+
return {disposition: "blocked", category: "quota"}
|
|
112
|
+
}
|
|
113
|
+
if (container.insufficient_balance === true) {
|
|
114
|
+
return {disposition: "blocked", category: "billing"}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract a safe retry duration in milliseconds. Only explicit millisecond
|
|
122
|
+
* fields (retryAfterMs, retry_after_ms) are used; ambiguous short fields such
|
|
123
|
+
* as retry_after (seconds) are never interpreted as milliseconds.
|
|
124
|
+
* @param {unknown} value
|
|
125
|
+
*/
|
|
126
|
+
function safeRetryMs(value) {
|
|
127
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return value
|
|
128
|
+
if (typeof value === "string" && /^\d+$/u.test(value)) {
|
|
129
|
+
const parsed = Number(value)
|
|
130
|
+
if (Number.isSafeInteger(parsed) && parsed > 0) return parsed
|
|
131
|
+
}
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
136
|
+
function isRecord(value) {
|
|
137
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
138
|
+
}
|
package/src/providers/kimi.js
CHANGED
|
@@ -67,6 +67,36 @@ export function createKimiSessionId() {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Provider-owned protocol completion. Kimi confirms a completed run with a
|
|
72
|
+
* final assistant response (assistant content without further tool calls) plus
|
|
73
|
+
* its session handoff record; the two records may arrive in either order. The
|
|
74
|
+
* detector reports completion exactly once and never treats intermediate tool
|
|
75
|
+
* activity as completion.
|
|
76
|
+
* @returns {(record: unknown) => boolean}
|
|
77
|
+
*/
|
|
78
|
+
export function createKimiCompletion() {
|
|
79
|
+
let finalResponseSeen = false
|
|
80
|
+
let resumeHintSeen = false
|
|
81
|
+
let completed = false
|
|
82
|
+
return (record) => {
|
|
83
|
+
if (completed || !isRecord(record)) return false
|
|
84
|
+
if (isFinalAssistantResponse(record)) finalResponseSeen = true
|
|
85
|
+
else if (recognizeKimiRecord(record, undefined).sessionId !== undefined) resumeHintSeen = true
|
|
86
|
+
if (!finalResponseSeen || !resumeHintSeen) return false
|
|
87
|
+
completed = true
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @param {Record<string, unknown>} record */
|
|
93
|
+
function isFinalAssistantResponse(record) {
|
|
94
|
+
return record.role === "assistant"
|
|
95
|
+
&& exactOptionalKeys(record, ["role"], ["content", "tool_calls"])
|
|
96
|
+
&& typeof record.content === "string"
|
|
97
|
+
&& record.tool_calls === undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
70
100
|
/** @returns {(record: unknown) => import("../types.js").WorkerEvent[]} */
|
|
71
101
|
export function createKimiParser() {
|
|
72
102
|
const state = createKimiRecordState()
|