threadwire 0.1.5 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +17 -4
- package/TELEGRAM-INGRESS.md +8 -2
- package/bin/isolated-runtime.js +5 -0
- package/bin/model-broker.js +5 -0
- package/docs/card-10520-plan.md +45 -0
- package/docs/container-runtime.md +3 -2
- package/docs/delegated-result-protocol.md +161 -0
- package/docs/evidence-artifacts.md +106 -0
- package/docs/isolated-provider-runtime.md +137 -0
- package/package.json +5 -1
- package/scripts/provider-shims/front-door.sh.template +18 -0
- package/scripts/verify-package.js +22 -1
- package/src/absolute-deadline.js +94 -0
- package/src/cli.js +362 -42
- package/src/context-budget-metrics.js +180 -0
- package/src/delegated-result-admission.js +377 -0
- package/src/docker-api.js +131 -0
- package/src/evidence-store.js +1472 -0
- package/src/isolated-runtime-client.js +149 -0
- package/src/isolated-runtime.js +982 -0
- package/src/isolated-state.js +409 -0
- package/src/isolated-worker.js +123 -0
- package/src/model-broker-policy.js +139 -0
- package/src/model-broker.js +313 -0
- package/src/mount-policy.js +28 -0
- package/src/normalized-output.js +68 -0
- package/src/notice-queue.js +22 -7
- package/src/providers/opencode.js +42 -18
- package/src/relay-write.js +44 -0
- package/src/relay.js +6 -6
- package/src/run-worker.js +158 -40
- package/src/telegram-ingress/command.js +16 -0
- package/src/telegram-ingress/config.js +96 -11
- package/src/telegram-ingress/core.js +201 -89
- package/src/telegram-ingress/http.js +17 -1
- package/src/telegram-webhook.js +22 -0
- package/src/types.js +2 -2
- package/src/worker-control.js +294 -0
- package/src/hermes-protocol.js +0 -126
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {NoticeQueue} from "./notice-queue.js"
|
|
4
|
+
import {Relay} from "./relay.js"
|
|
5
|
+
|
|
6
|
+
export class WorkerControl {
|
|
7
|
+
/**
|
|
8
|
+
* @param {{
|
|
9
|
+
* sender: import("./types.js").NoticeSender,
|
|
10
|
+
* processNumber: number,
|
|
11
|
+
* toolMessages?: boolean,
|
|
12
|
+
* maxOutputLength?: number,
|
|
13
|
+
* sentenceLatencyMs?: number,
|
|
14
|
+
* maxAssistantBufferLength?: number,
|
|
15
|
+
* setTimer?: (callback: () => void, milliseconds: number) => unknown,
|
|
16
|
+
* clearTimer?: (handle: unknown) => void,
|
|
17
|
+
* NoticeQueueClass?: typeof NoticeQueue,
|
|
18
|
+
* RelayClass?: typeof Relay,
|
|
19
|
+
* metrics?: import("./context-budget-metrics.js").ContextBudgetMetrics,
|
|
20
|
+
* queueOptions?: Omit<ConstructorParameters<typeof NoticeQueue>[0], "sender" | "onDelivered">
|
|
21
|
+
* }} options
|
|
22
|
+
*/
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.processLabel = `[P${options.processNumber}]`
|
|
25
|
+
this.metrics = options.metrics
|
|
26
|
+
this.toolMessages = options.toolMessages ?? false
|
|
27
|
+
this.saturationText = `${this.processLabel} Notifier saturated; coalescing progress updates.`
|
|
28
|
+
this.failure = undefined
|
|
29
|
+
this.saturated = false
|
|
30
|
+
this.saturationNoticeSent = false
|
|
31
|
+
this.coalescedNotice = undefined
|
|
32
|
+
/** @type {import("./types.js").RenderedNotice[]} */
|
|
33
|
+
this.renderedNotices = []
|
|
34
|
+
/** @type {Array<() => void>} */
|
|
35
|
+
this.availabilityWaiters = []
|
|
36
|
+
/** @type {Promise<void>} */
|
|
37
|
+
this.operation = Promise.resolve()
|
|
38
|
+
/** @type {Promise<void> | undefined} */
|
|
39
|
+
this.finalizePromise = undefined
|
|
40
|
+
/** @type {Promise<void> | undefined} */
|
|
41
|
+
this.closePromise = undefined
|
|
42
|
+
const sender = {
|
|
43
|
+
/** @param {string | import("./types.js").OutgoingMessage} message */
|
|
44
|
+
send: async (message) => {
|
|
45
|
+
const sent = await options.sender.send(message)
|
|
46
|
+
this.noteAvailability()
|
|
47
|
+
return sent
|
|
48
|
+
},
|
|
49
|
+
...(options.sender.edit === undefined
|
|
50
|
+
? {}
|
|
51
|
+
: {
|
|
52
|
+
/** @param {number} messageId @param {string | import("./types.js").OutgoingMessage} message */
|
|
53
|
+
edit: async (messageId, message) => {
|
|
54
|
+
await options.sender.edit?.(messageId, message)
|
|
55
|
+
this.noteAvailability()
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
const NoticeQueueClass = options.NoticeQueueClass ?? NoticeQueue
|
|
60
|
+
const RelayClass = options.RelayClass ?? Relay
|
|
61
|
+
this.queue = new NoticeQueueClass({
|
|
62
|
+
sender,
|
|
63
|
+
...(options.queueOptions ?? {}),
|
|
64
|
+
onDelivered: (metricClass, bytes) => this.metrics?.recordProgress(metricClass, bytes, "delivered")
|
|
65
|
+
})
|
|
66
|
+
this.maxCoalescedBytes = this.queue.maxPendingBytes
|
|
67
|
+
const relayQueue = {
|
|
68
|
+
maxLength: this.queue.maxLength,
|
|
69
|
+
enqueue: /** @param {import("./types.js").RenderedNotice} notice */ (notice) => {
|
|
70
|
+
this.renderedNotices.push(notice)
|
|
71
|
+
this.scheduleBackgroundFlush()
|
|
72
|
+
},
|
|
73
|
+
close: async () => this.finalizeInternal()
|
|
74
|
+
}
|
|
75
|
+
this.relay = new RelayClass({
|
|
76
|
+
queue: /** @type {import("./notice-queue.js").NoticeQueue} */ (/** @type {unknown} */ (relayQueue)),
|
|
77
|
+
processNumber: options.processNumber,
|
|
78
|
+
...(options.toolMessages === undefined ? {} : {toolMessages: options.toolMessages}),
|
|
79
|
+
...(options.maxOutputLength === undefined ? {} : {maxOutputLength: options.maxOutputLength}),
|
|
80
|
+
...(options.sentenceLatencyMs === undefined ? {} : {sentenceLatencyMs: options.sentenceLatencyMs}),
|
|
81
|
+
...(options.maxAssistantBufferLength === undefined ? {} : {maxAssistantBufferLength: options.maxAssistantBufferLength}),
|
|
82
|
+
...(options.setTimer === undefined ? {} : {setTimer: options.setTimer}),
|
|
83
|
+
...(options.clearTimer === undefined ? {} : {clearTimer: options.clearTimer})
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @param {import("./types.js").WorkerEvent} event */
|
|
88
|
+
accept(event) {
|
|
89
|
+
const metricClass = event.type === "text-delta" ? "assistant" : event.type
|
|
90
|
+
const bytes = Buffer.byteLength(JSON.stringify(event), "utf8")
|
|
91
|
+
this.metrics?.recordProgress(metricClass, bytes, "attempted")
|
|
92
|
+
if (event.type === "tool" && !this.toolMessages) this.metrics?.recordProgress("tool", bytes, "suppressed")
|
|
93
|
+
return this.enqueueOperation(async () => {
|
|
94
|
+
this.ensureHealthy()
|
|
95
|
+
this.relay.accept(event)
|
|
96
|
+
await this.flushLoop(false)
|
|
97
|
+
this.ensureHealthy()
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
close() {
|
|
102
|
+
if (!this.closePromise) {
|
|
103
|
+
this.closePromise = this.enqueueOperation(async () => {
|
|
104
|
+
await this.relay.close()
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
return this.closePromise
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
noteAvailability() {
|
|
111
|
+
this.signalAvailability()
|
|
112
|
+
if (this.failure || this.finalizePromise || this.queue.failure || this.queue.closed) return
|
|
113
|
+
this.scheduleBackgroundFlush()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
scheduleBackgroundFlush() {
|
|
117
|
+
void this.enqueueOperation(async () => {
|
|
118
|
+
await this.flushLoop(false)
|
|
119
|
+
}).catch((error) => {
|
|
120
|
+
this.failure = toError(error)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** @param {() => Promise<void>} operation */
|
|
125
|
+
enqueueOperation(operation) {
|
|
126
|
+
const next = this.operation.then(operation, operation)
|
|
127
|
+
this.operation = next.catch(() => {})
|
|
128
|
+
return next
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async finalizeInternal() {
|
|
132
|
+
if (!this.finalizePromise) {
|
|
133
|
+
this.finalizePromise = (async () => {
|
|
134
|
+
await this.flushLoop(true)
|
|
135
|
+
await this.queue.close()
|
|
136
|
+
if (this.failure) throw this.failure
|
|
137
|
+
})()
|
|
138
|
+
}
|
|
139
|
+
return this.finalizePromise
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @param {boolean} drainAll */
|
|
143
|
+
async flushLoop(drainAll) {
|
|
144
|
+
while (true) {
|
|
145
|
+
this.ensureHealthy()
|
|
146
|
+
if (this.tryFlushSaturationNotice()) continue
|
|
147
|
+
if (this.renderedNotices.length > 0) {
|
|
148
|
+
const notice = this.renderedNotices[0]
|
|
149
|
+
if (!notice) return
|
|
150
|
+
if (isCoalescibleAssistantProgress(notice, this.processLabel)) {
|
|
151
|
+
this.renderedNotices.shift()
|
|
152
|
+
if (this.canResumeHealthyProgress() && this.tryEnqueue(notice)) {
|
|
153
|
+
this.clearSaturation()
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
if (!this.saturated && this.tryEnqueue(notice)) continue
|
|
157
|
+
this.saturated = true
|
|
158
|
+
this.metrics?.recordProgress("assistant", Buffer.byteLength(notice.text, "utf8"), "coalesced")
|
|
159
|
+
this.coalescedNotice = mergeCoalescedNotice(this.coalescedNotice, notice, this.maxCoalescedBytes)
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if (this.tryEnqueue(notice)) {
|
|
163
|
+
this.renderedNotices.shift()
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
await this.waitForAvailability()
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
if (this.saturated && !this.saturationNoticeSent) {
|
|
170
|
+
if (!drainAll) return
|
|
171
|
+
await this.waitForAvailability()
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
if (this.coalescedNotice !== undefined) {
|
|
175
|
+
if (this.tryEnqueue(this.coalescedNotice)) {
|
|
176
|
+
this.coalescedNotice = undefined
|
|
177
|
+
this.clearSaturation()
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
if (!drainAll) return
|
|
181
|
+
await this.waitForAvailability()
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
tryFlushSaturationNotice() {
|
|
189
|
+
if (!this.saturated || this.saturationNoticeSent) return false
|
|
190
|
+
const notice = {text: this.saturationText, label: `${this.processLabel} `}
|
|
191
|
+
if (!this.tryEnqueue(notice)) return false
|
|
192
|
+
this.saturationNoticeSent = true
|
|
193
|
+
return true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
canResumeHealthyProgress() {
|
|
197
|
+
return this.saturated && this.coalescedNotice === undefined && this.saturationNoticeSent
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
clearSaturation() {
|
|
201
|
+
this.saturated = false
|
|
202
|
+
this.saturationNoticeSent = false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @param {import("./types.js").RenderedNotice} notice */
|
|
206
|
+
tryEnqueue(notice) {
|
|
207
|
+
const noticeBytes = Buffer.byteLength(notice.text, "utf8")
|
|
208
|
+
if (noticeBytes > this.queue.maxPendingBytes) {
|
|
209
|
+
this.queue.enqueue(notice)
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
if (this.queue.pendingNoticeCount >= this.queue.maxPendingNotices) return false
|
|
213
|
+
if (this.queue.pendingBytes + noticeBytes > this.queue.maxPendingBytes) return false
|
|
214
|
+
this.queue.enqueue(notice)
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async waitForAvailability() {
|
|
219
|
+
this.ensureHealthy()
|
|
220
|
+
if (!this.queue.processing) return
|
|
221
|
+
await Promise.race([
|
|
222
|
+
this.queue.work ?? Promise.resolve(),
|
|
223
|
+
new Promise((resolve) => { this.availabilityWaiters.push(() => resolve(undefined)) })
|
|
224
|
+
])
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
signalAvailability() {
|
|
228
|
+
const waiters = this.availabilityWaiters.splice(0)
|
|
229
|
+
for (const resolve of waiters) resolve()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
ensureHealthy() {
|
|
233
|
+
if (this.failure) throw this.failure
|
|
234
|
+
if (this.queue.failure) throw this.queue.failure
|
|
235
|
+
if (this.queue.closed) throw new Error("Notice queue is closed")
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {import("./types.js").RenderedNotice} notice
|
|
241
|
+
* @param {string} processLabel
|
|
242
|
+
*/
|
|
243
|
+
function isCoalescibleAssistantProgress(notice, processLabel) {
|
|
244
|
+
return !("activity" in notice) && notice.format === undefined && notice.label === `${processLabel} Assistant: `
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {import("./types.js").TextNotice | undefined} current
|
|
249
|
+
* @param {import("./types.js").TextNotice} incoming
|
|
250
|
+
* @param {number} maxBytes
|
|
251
|
+
* @returns {import("./types.js").TextNotice}
|
|
252
|
+
*/
|
|
253
|
+
function mergeCoalescedNotice(current, incoming, maxBytes) {
|
|
254
|
+
const next = trimAssistantNotice(incoming, maxBytes)
|
|
255
|
+
if (
|
|
256
|
+
current === undefined
|
|
257
|
+
|| current.format !== incoming.format
|
|
258
|
+
|| current.label !== incoming.label
|
|
259
|
+
|| current.batchSeparator !== incoming.batchSeparator
|
|
260
|
+
) return next
|
|
261
|
+
const combined = {...current, text: `${current.text}${incoming.text}`}
|
|
262
|
+
if (Buffer.byteLength(combined.text, "utf8") <= maxBytes) return combined
|
|
263
|
+
return next
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** @param {import("./types.js").TextNotice} notice @param {number} maxBytes */
|
|
267
|
+
function trimAssistantNotice(notice, maxBytes) {
|
|
268
|
+
if (Buffer.byteLength(notice.text, "utf8") <= maxBytes) return notice
|
|
269
|
+
const label = notice.label ?? ""
|
|
270
|
+
const body = notice.text.startsWith(label) ? notice.text.slice(label.length) : notice.text
|
|
271
|
+
const available = Math.max(0, maxBytes - Buffer.byteLength(label, "utf8"))
|
|
272
|
+
return {...notice, text: `${label}${takeUtf8Prefix(body, available)}`}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** @param {string} text @param {number} maxBytes */
|
|
276
|
+
function takeUtf8Prefix(text, maxBytes) {
|
|
277
|
+
let end = 0
|
|
278
|
+
let bytes = 0
|
|
279
|
+
while (end < text.length) {
|
|
280
|
+
const codePoint = text.codePointAt(end)
|
|
281
|
+
if (codePoint === undefined) break
|
|
282
|
+
const character = String.fromCodePoint(codePoint)
|
|
283
|
+
const nextBytes = Buffer.byteLength(character, "utf8")
|
|
284
|
+
if (bytes + nextBytes > maxBytes) break
|
|
285
|
+
bytes += nextBytes
|
|
286
|
+
end += character.length
|
|
287
|
+
}
|
|
288
|
+
return text.slice(0, end)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** @param {unknown} error */
|
|
292
|
+
function toError(error) {
|
|
293
|
+
return error instanceof Error ? error : new Error("Worker control failed")
|
|
294
|
+
}
|
package/src/hermes-protocol.js
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
import {redactSecrets, sanitizeControl, sanitizeOutput} from "./relay.js"
|
|
4
|
-
|
|
5
|
-
export const MAX_ASSISTANT_TEXT_LENGTH = 16_384
|
|
6
|
-
export const MAX_TOOL_NAME_LENGTH = 128
|
|
7
|
-
export const MAX_TOOL_DETAIL_LENGTH = 1_024
|
|
8
|
-
export const MAX_TOOL_OUTPUT_LENGTH = 4_096
|
|
9
|
-
export const MAX_PROGRESS_RECORDS = 64
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Low-volume, caller-facing JSONL projection of normalized worker events.
|
|
13
|
-
* Text deltas are retained only for the terminal result; raw provider records
|
|
14
|
-
* never enter this component.
|
|
15
|
-
*/
|
|
16
|
-
export class HermesProtocol {
|
|
17
|
-
/** @param {{output: NodeJS.WritableStream}} options */
|
|
18
|
-
constructor(options) {
|
|
19
|
-
this.output = options.output
|
|
20
|
-
this.progressRecords = 0
|
|
21
|
-
this.assistantText = ""
|
|
22
|
-
this.assistantTextTruncated = false
|
|
23
|
-
/** @type {Set<string>} */
|
|
24
|
-
this.transitions = new Set()
|
|
25
|
-
/** @type {Set<string>} */
|
|
26
|
-
this.lifecyclePhases = new Set()
|
|
27
|
-
/** @type {Map<string, {name: string, detail?: string}>} */
|
|
28
|
-
this.activeTools = new Map()
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** @param {import("./types.js").WorkerEvent} event */
|
|
32
|
-
accept(event) {
|
|
33
|
-
if (event.type === "text-delta") {
|
|
34
|
-
this.appendAssistantText(event.text)
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
if (this.progressRecords >= MAX_PROGRESS_RECORDS) return
|
|
38
|
-
if (event.type === "lifecycle") {
|
|
39
|
-
if (this.lifecyclePhases.has(event.phase)) return
|
|
40
|
-
this.lifecyclePhases.add(event.phase)
|
|
41
|
-
this.emit({type: lifecycleType(event.phase), summary: safeSingleLine(event.summary, 512)})
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
if (event.type === "diagnostic") {
|
|
45
|
-
this.emit({type: "diagnostic", level: event.level, summary: safeSingleLine(event.summary, 512)})
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
const transition = `${event.key}\0${event.phase}`
|
|
49
|
-
if (this.transitions.has(transition)) return
|
|
50
|
-
this.transitions.add(transition)
|
|
51
|
-
const current = this.activeTools.get(event.key)
|
|
52
|
-
const name = (current?.name ?? safeSingleLine(event.name, MAX_TOOL_NAME_LENGTH)) || "(tool)"
|
|
53
|
-
const incomingDetail = safeOptionalSingleLine(event.detail, MAX_TOOL_DETAIL_LENGTH)
|
|
54
|
-
const detail = current?.detail ?? incomingDetail
|
|
55
|
-
if (event.phase === "started") this.activeTools.set(event.key, {name, ...(detail === undefined ? {} : {detail})})
|
|
56
|
-
else this.activeTools.delete(event.key)
|
|
57
|
-
const output = event.phase === "finished" ? safeOptionalOutput(event.output, MAX_TOOL_OUTPUT_LENGTH) : undefined
|
|
58
|
-
this.emit({
|
|
59
|
-
type: event.phase === "started" ? "tool_started" : "tool_finished",
|
|
60
|
-
name,
|
|
61
|
-
...(detail === undefined ? {} : {detail}),
|
|
62
|
-
...(output === undefined ? {} : {output})
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** @param {number} exitCode */
|
|
67
|
-
complete(exitCode) {
|
|
68
|
-
const assistantText = safeOptionalOutput(this.assistantText, MAX_ASSISTANT_TEXT_LENGTH, this.assistantTextTruncated)
|
|
69
|
-
this.write({type: "completed", exitCode, ...(assistantText === undefined ? {} : {assistantText})})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** @param {string} text */
|
|
73
|
-
appendAssistantText(text) {
|
|
74
|
-
if (this.assistantTextTruncated) return
|
|
75
|
-
const remaining = MAX_ASSISTANT_TEXT_LENGTH - Array.from(this.assistantText).length
|
|
76
|
-
const characters = Array.from(text)
|
|
77
|
-
if (characters.length <= remaining) this.assistantText += text
|
|
78
|
-
else {
|
|
79
|
-
this.assistantText += characters.slice(0, Math.max(0, remaining - 1)).join("") + "…"
|
|
80
|
-
this.assistantTextTruncated = true
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** @param {Record<string, unknown>} record */
|
|
85
|
-
emit(record) {
|
|
86
|
-
this.progressRecords += 1
|
|
87
|
-
this.write(record)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** @param {Record<string, unknown>} record */
|
|
91
|
-
write(record) {
|
|
92
|
-
this.output.write(`${JSON.stringify(record)}\n`)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** @param {"started" | "completed" | "failed"} phase */
|
|
97
|
-
function lifecycleType(phase) {
|
|
98
|
-
return phase === "started" ? "worker_started" : phase === "failed" ? "worker_failed" : "worker_completed"
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** @param {string} text @param {number} limit */
|
|
102
|
-
function safeSingleLine(text, limit) {
|
|
103
|
-
return limitText(redactSecrets(sanitizeControl(text)), limit)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** @param {string | undefined} text @param {number} limit */
|
|
107
|
-
function safeOptionalSingleLine(text, limit) {
|
|
108
|
-
if (text === undefined) return undefined
|
|
109
|
-
const safe = safeSingleLine(text, limit)
|
|
110
|
-
return safe.trim().length === 0 ? undefined : safe
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** @param {string | undefined} text @param {number} limit @param {boolean} [alreadyTruncated] */
|
|
114
|
-
function safeOptionalOutput(text, limit, alreadyTruncated = false) {
|
|
115
|
-
if (text === undefined) return undefined
|
|
116
|
-
const safe = sanitizeOutput(text)
|
|
117
|
-
if (safe === undefined) return undefined
|
|
118
|
-
return alreadyTruncated ? safe : limitText(safe, limit)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** @param {string} text @param {number} limit */
|
|
122
|
-
function limitText(text, limit) {
|
|
123
|
-
const characters = Array.from(text)
|
|
124
|
-
if (characters.length <= limit) return text
|
|
125
|
-
return `${characters.slice(0, limit - 1).join("")}…`
|
|
126
|
-
}
|