@gitroll/tacitt-cli 0.6.8 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{agent-setup-YlFIJz-h.mjs → agent-setup-LPd8WEsN.mjs} +2 -2
- package/dist/agent-setup-worker.mjs +5 -5
- package/dist/cli.mjs +19 -10
- package/dist/codex-title-worker.mjs +1 -1
- package/dist/configured-preview-worker.mjs +5 -5
- package/dist/configured-session-worker.mjs +5 -5
- package/dist/{frame-Dj5Zs6UW.mjs → frame-ByT1xayJ.mjs} +1 -1
- package/dist/{heap-DsE74_-n.mjs → heap-CM298Yu0.mjs} +8 -6
- package/dist/inspection-worker.mjs +4 -4
- package/dist/{job-CeSf9UCC.mjs → job-BHIwElNF.mjs} +5 -5
- package/dist/{locate-BWkblijS.mjs → locate-Cwv6WH8n.mjs} +25 -2
- package/dist/{model-f_QnDyyb.mjs → model-zvn5GUJ9.mjs} +1 -1
- package/dist/operation-executor.mjs +3 -3
- package/dist/preview-worker.mjs +1 -1
- package/dist/privacy-validator.mjs +1 -1
- package/dist/{query-CaBXQlpF.mjs → query-By41gaKm.mjs} +585 -250
- package/dist/{run-RA_8zZsL.mjs → run-ZQx7p4Rw.mjs} +4 -4
- package/dist/{src-p_uJNjV3.mjs → src-3jvMvSQn.mjs} +3 -3
- package/dist/{src-d7n58sbH.mjs → src-BqtdFVq7.mjs} +683 -341
- package/dist/{src-BBjnljUs.mjs → src-D_DfY--2.mjs} +4 -4
- package/dist/store-database-worker.mjs +1 -1
- package/dist/web/assets/index-B14e6_-z.js +174 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/skills/tacitt/INDEX.md +6 -6
- package/skills/tacitt/SKILL.md +4 -4
- package/skills/tacitt/references/INDEX.md +3 -3
- package/skills/tacitt/references/classification.md +12 -2
- package/skills/tacitt/references/commands.md +60 -13
- package/skills/tacitt/references/journey.md +10 -2
- package/skills/tacitt/references/parallel-review.md +21 -0
- package/skills/tacitt/references/send.md +29 -8
- package/skills/tacitt/scripts/INDEX.md +39 -36
- package/skills/tacitt/scripts/journey-classification-validation.d.mts +13 -0
- package/skills/tacitt/scripts/journey-classification-validation.mjs +38 -0
- package/skills/tacitt/scripts/journey-scan-timing.d.mts +15 -0
- package/skills/tacitt/scripts/journey-scan-timing.mjs +153 -1
- package/skills/tacitt/scripts/journey-state.d.mts +6 -0
- package/skills/tacitt/scripts/journey-state.mjs +24 -1
- package/skills/tacitt/scripts/journey.d.mts +5 -0
- package/skills/tacitt/scripts/journey.mjs +48 -3
- package/skills/tacitt/scripts/privacy-validator.mjs +11 -1
- package/skills/tacitt/scripts/review.mjs +294 -38
- package/LICENSE +0 -11
- package/dist/web/assets/index-B6kJ6XXF.js +0 -174
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
phaseOf,
|
|
8
8
|
uncoveredPending,
|
|
9
9
|
} from './journey-state.mjs'
|
|
10
|
+
import { observeScanTiming } from './journey-scan-timing.mjs'
|
|
10
11
|
import { startReviewStream } from './review-stream-client.mjs'
|
|
11
12
|
import { createHash, randomUUID } from 'node:crypto'
|
|
12
13
|
import {
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
readdir,
|
|
20
21
|
realpath,
|
|
21
22
|
rename,
|
|
23
|
+
stat,
|
|
22
24
|
rm,
|
|
23
25
|
writeFile,
|
|
24
26
|
} from 'node:fs/promises'
|
|
@@ -48,13 +50,10 @@ const SLOTS = 16
|
|
|
48
50
|
// While `--agents auto` waits, journey input (newly published sessions) is rechecked this often.
|
|
49
51
|
const AUTO_RECHECK_MS = 3_000
|
|
50
52
|
const PREPARATION_WAIT_MS = 60_000
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
// A pack with no published answer this long after dispatch has likely lost its child (for
|
|
56
|
-
// example to a parent restart); packs finished within about two minutes on the 2026-10-26 corpus.
|
|
57
|
-
const STALE_MS = 300_000
|
|
53
|
+
// Bound from reservation, allowing the observed long tail of completed children.
|
|
54
|
+
const LOST_PACK_TIMEOUT_MS = 450_000
|
|
55
|
+
// A current draft renews liveness independently of the reservation age.
|
|
56
|
+
const DRAFT_HEARTBEAT_TIMEOUT_MS = 450_000
|
|
58
57
|
const WRAP = 400
|
|
59
58
|
// `review.terms` bounds: a value has 1–4000 code points, an exclusion reason 500.
|
|
60
59
|
const TERM_CODE_POINTS = 4000
|
|
@@ -86,9 +85,21 @@ class HelperError extends Error {
|
|
|
86
85
|
super(code)
|
|
87
86
|
this.code = /^[A-Z_]{3,80}$/u.test(code) ? code : 'HELPER_FAILED'
|
|
88
87
|
if (
|
|
89
|
-
code === 'INVALID_REVIEW_SPAN' &&
|
|
90
88
|
typeof field === 'string' &&
|
|
91
|
-
/^terms\[\d+(?:,\d+){0,31}\]$/u.test(field)
|
|
89
|
+
((code === 'INVALID_REVIEW_SPAN' && /^terms\[\d+(?:,\d+){0,31}\]$/u.test(field)) ||
|
|
90
|
+
(code === 'INVALID_SELECTION' &&
|
|
91
|
+
[
|
|
92
|
+
'cards.append.empty',
|
|
93
|
+
'cards.append.limit',
|
|
94
|
+
'cards.append.duplicate-card',
|
|
95
|
+
'cards.append.existing-card',
|
|
96
|
+
'cards.append.duplicate-extension',
|
|
97
|
+
'cards.append.unknown-card',
|
|
98
|
+
'cards.append.duplicate-term',
|
|
99
|
+
'cards.append.term-not-open',
|
|
100
|
+
'cards.append.own-identity-card',
|
|
101
|
+
'cards.append.other-person',
|
|
102
|
+
].includes(field)))
|
|
92
103
|
)
|
|
93
104
|
this.field = field
|
|
94
105
|
}
|
|
@@ -301,7 +312,17 @@ async function observe(ctx, readOnly = false) {
|
|
|
301
312
|
ctx.state.preparationFailed = false
|
|
302
313
|
if (ctx.state.reader) ctx.state.reader.waitingSince = null
|
|
303
314
|
}
|
|
304
|
-
if (!readOnly)
|
|
315
|
+
if (!readOnly) {
|
|
316
|
+
ctx.state.completedWorks = completedWorks
|
|
317
|
+
ctx.state.preparationBlocked = [
|
|
318
|
+
...new Set([
|
|
319
|
+
...(ctx.state.preparationBlocked ?? []),
|
|
320
|
+
...view.works
|
|
321
|
+
.filter((work) => work.status === 'blocked')
|
|
322
|
+
.map((work) => `${work.id}:${work.inputRevision}`),
|
|
323
|
+
]),
|
|
324
|
+
]
|
|
325
|
+
}
|
|
305
326
|
const works = Object.fromEntries(
|
|
306
327
|
view.works.map((work) => [work.id, { inputRevision: work.inputRevision, access: work.access }]),
|
|
307
328
|
)
|
|
@@ -587,7 +608,7 @@ const withAnswer = (task, text) => {
|
|
|
587
608
|
// Each pack names its own draft so concurrent children never share a scratch file.
|
|
588
609
|
const draft = resolve(task.replace(/\.md$/u, '.draft.json'))
|
|
589
610
|
const command = `${shellQuote(process.execPath)} ${shellQuote(resolve(task.replace(/\.md$/u, '.publish.mjs')))} ${shellQuote(draft)}`
|
|
590
|
-
return `${text}\n\n## Your answer\n\
|
|
611
|
+
return `${text}\n\n## Your answer\n\nAs soon as your reads return, overwrite ${draft} with {} again: the CLI treats this file as your progress signal and re-dispatches a task whose draft stops changing. Then write the complete JSON object to ${draft} using your native file-edit tool. Publish it with this exact command:\n\n${command}\n\nThe CLI validates task coverage and every normalized term against the assigned review source before atomically publishing. Invalid source diagnostics identify normalized terms indices and a private normalized-answer receipt; read that receipt because these indices may differ from the original draft after splitting. Preserve supported findings, repair exact contiguous text, and explicitly report unsupported or context-only proposals. Never silently discard a finding to pass validation. Do not write the canonical answer yourself. Correct validation failures in your draft and retry the same command. Reply with only done after successful publication; otherwise report failure.\n`
|
|
591
612
|
}
|
|
592
613
|
/** Validate a complete task answer before the publisher replaces any prior answer. */
|
|
593
614
|
export function validatePrivacyAnswer(answer, unitRefs) {
|
|
@@ -656,7 +677,7 @@ function packPrompt(task, text) {
|
|
|
656
677
|
const reads = plan.map(({ offset, limit }) => `(${offset},${limit})`).join(' ')
|
|
657
678
|
// In the prompt itself, not a start file: given the plan indirectly, fast models read the
|
|
658
679
|
// ranges one turn at a time and then reread them, and each extra turn costs seconds.
|
|
659
|
-
return `Your FIRST message must contain exactly ${plan.length} Read tool calls issued together in parallel on ${task} with these (offset, limit): ${reads}. Do not read it any other way. Allow 24000 output tokens per read; with functions.exec, start its code with // @exec: {"max_output_tokens": 24000} and set the nested exec_command max_output_tokens to 24000. Reread a refused or truncated range in smaller parts. Then follow that task exactly: author the JSON draft with your file-edit tool and run only its exact publisher command. Work alone. Reply done after successful publication.`
|
|
680
|
+
return `Your FIRST message must contain exactly ${plan.length} Read tool calls issued together in parallel on ${task} with these (offset, limit): ${reads}, plus one file-edit call that creates ${resolve(task.replace(/\.md$/u, '.draft.json'))} containing {} (your progress signal: a task whose draft stops changing is re-dispatched). Do not read it any other way. Allow 24000 output tokens per read; with functions.exec, start its code with // @exec: {"max_output_tokens": 24000} and set the nested exec_command max_output_tokens to 24000. Reread a refused or truncated range in smaller parts. Then follow that task exactly: author the JSON draft with your file-edit tool and run only its exact publisher command. Work alone. Reply done after successful publication.`
|
|
660
681
|
}
|
|
661
682
|
/** Writes a task and its publisher and returns the agent's prompt. */
|
|
662
683
|
export async function writePackTask(task, text, unitRefs, sourceBatches = []) {
|
|
@@ -908,6 +929,8 @@ async function finishPack(ctx, meta) {
|
|
|
908
929
|
ctx.state.queue.unshift(...requeue)
|
|
909
930
|
meta.units = kept
|
|
910
931
|
meta.state = kept.length ? 'open' : 'empty'
|
|
932
|
+
// The lease starts when the task is handed out, also for a pack resumed from dispatching.
|
|
933
|
+
meta.dispatchedAt = new Date().toISOString()
|
|
911
934
|
meta.codePoints = batches.reduce(
|
|
912
935
|
(total, item) => total + item.unit.coreCodePoints + item.unit.contextCodePoints,
|
|
913
936
|
0,
|
|
@@ -958,7 +981,9 @@ async function waiting(ctx, pack) {
|
|
|
958
981
|
/** Every open pack whose agent wrote its answer, or whose saved answer is not recorded yet. */
|
|
959
982
|
async function ingestAll(ctx) {
|
|
960
983
|
const changes = await observe(ctx)
|
|
984
|
+
await reclaimLostPacks(ctx)
|
|
961
985
|
const ingested = []
|
|
986
|
+
const freedSlots = new Set((ctx.lost ?? []).map((pack) => pack.packId))
|
|
962
987
|
let stopped = false
|
|
963
988
|
const terms = { added: 0, kept: 0, unlocated: 0 }
|
|
964
989
|
for (const pack of await readAll(join(ctx.run, 'packs'))) {
|
|
@@ -981,6 +1006,13 @@ async function ingestAll(ctx) {
|
|
|
981
1006
|
})
|
|
982
1007
|
continue
|
|
983
1008
|
}
|
|
1009
|
+
if (
|
|
1010
|
+
!pack.units.some(({ batchId }) => {
|
|
1011
|
+
const unit = ctx.units.get(batchId)
|
|
1012
|
+
return unit?.packId === pack.id && unit.state === 'dispatched'
|
|
1013
|
+
})
|
|
1014
|
+
)
|
|
1015
|
+
freedSlots.add(pack.id)
|
|
984
1016
|
for (const key of Object.keys(terms)) terms[key] += result.terms[key]
|
|
985
1017
|
ingested.push({
|
|
986
1018
|
packId: pack.id,
|
|
@@ -1001,6 +1033,8 @@ async function ingestAll(ctx) {
|
|
|
1001
1033
|
if (!paused(ctx) && !stopped && !overBudget(ctx)) await readNext(ctx)
|
|
1002
1034
|
return {
|
|
1003
1035
|
changes,
|
|
1036
|
+
lost: ctx.lost ?? [],
|
|
1037
|
+
freedSlots: [...freedSlots],
|
|
1004
1038
|
ingested,
|
|
1005
1039
|
terms,
|
|
1006
1040
|
...(await progressOf(ctx)),
|
|
@@ -1009,6 +1043,42 @@ async function ingestAll(ctx) {
|
|
|
1009
1043
|
next: stopped ? 'ingest' : await suggest(ctx),
|
|
1010
1044
|
}
|
|
1011
1045
|
}
|
|
1046
|
+
async function scanTiming(ctx, cardAgents, ingestStartedAt) {
|
|
1047
|
+
const receipt = await readJson(join(ctx.run, 'scan-timing.json'), null)
|
|
1048
|
+
if (!receipt || receipt.completedAt) return
|
|
1049
|
+
const packs = []
|
|
1050
|
+
for (const pack of await readAll(join(ctx.run, 'packs'))) {
|
|
1051
|
+
let publishedAt = null
|
|
1052
|
+
try {
|
|
1053
|
+
publishedAt = (await stat(answerPath(pack.retryTask ?? pack.task))).mtimeMs
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
if (error.code !== 'ENOENT') throw error
|
|
1056
|
+
}
|
|
1057
|
+
const active =
|
|
1058
|
+
pack.state === 'open' &&
|
|
1059
|
+
pack.units.some(({ batchId }) => {
|
|
1060
|
+
const unit = ctx.units.get(batchId)
|
|
1061
|
+
return unit?.packId === pack.id && unit.state === 'dispatched'
|
|
1062
|
+
})
|
|
1063
|
+
if ((active || publishedAt !== null) && Number.isFinite(Date.parse(pack.dispatchedAt)))
|
|
1064
|
+
packs.push({ id: pack.id, dispatchedAt: pack.dispatchedAt, publishedAt })
|
|
1065
|
+
}
|
|
1066
|
+
await observeScanTiming(ctx.run, {
|
|
1067
|
+
at: Date.now(),
|
|
1068
|
+
...(ingestStartedAt === undefined ? {} : { ingestStartedAt }),
|
|
1069
|
+
packs,
|
|
1070
|
+
activeCardChildren: cardAgents ?? receipt.measurements?.last?.activeCardChildren ?? 0,
|
|
1071
|
+
queuedBatches: ctx.state.queue.length,
|
|
1072
|
+
unreadBatches: ctx.state.reader?.remaining ?? 0,
|
|
1073
|
+
waiting: ctx.state.reader?.waiting,
|
|
1074
|
+
pendingWorkIds: ctx.view.works
|
|
1075
|
+
.filter(
|
|
1076
|
+
(work) =>
|
|
1077
|
+
work.access !== 'drop' && work.rootSessionIds.length > 0 && work.status === 'pending',
|
|
1078
|
+
)
|
|
1079
|
+
.map((work) => work.id),
|
|
1080
|
+
})
|
|
1081
|
+
}
|
|
1012
1082
|
/** Refill only confirmed free native slots, after exact ingestion has settled. */
|
|
1013
1083
|
async function ingestAndRefill(ctx, options) {
|
|
1014
1084
|
const auto = options.agents === 'auto'
|
|
@@ -1017,16 +1087,18 @@ async function ingestAndRefill(ctx, options) {
|
|
|
1017
1087
|
fail('INVALID_ARGUMENTS')
|
|
1018
1088
|
const result = await ingestAll(ctx)
|
|
1019
1089
|
const outstanding = await outstandingPacks(ctx)
|
|
1020
|
-
const
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1090
|
+
const lostSlots = result.lost.length
|
|
1091
|
+
const available = Math.max(
|
|
1092
|
+
0,
|
|
1093
|
+
SLOTS - outstanding.length - Number(options['card-agents'] ?? 0) - lostSlots,
|
|
1094
|
+
)
|
|
1095
|
+
const plannedAgents = result.cardBatch.ready
|
|
1096
|
+
? Math.min(available, Number(options['planned-card-agents'] ?? 1))
|
|
1097
|
+
: 0
|
|
1098
|
+
result.cardBatch.plannedAgents = plannedAgents
|
|
1099
|
+
// An explicit count never takes live leases past the sixteen slots (ADR 0179).
|
|
1100
|
+
const agents = auto ? available - plannedAgents : Math.min(requested, available - plannedAgents)
|
|
1101
|
+
const slots = auto ? { freeSlots: agents } : {}
|
|
1030
1102
|
const unsettled = result.ingested.some(
|
|
1031
1103
|
(item) =>
|
|
1032
1104
|
item.error ||
|
|
@@ -1082,19 +1154,22 @@ async function autoRefill(ctx, options) {
|
|
|
1082
1154
|
// ADR 0234: a card batch can come due on time alone; neither gathering slots nor waiting for a
|
|
1083
1155
|
// child holds it back past that time.
|
|
1084
1156
|
await progressOf(ctx)
|
|
1085
|
-
|
|
1157
|
+
// A batch already due wakes nothing more: it waits for a free slot like any refill.
|
|
1158
|
+
const wake = () => (ctx.cardsDueAt > Date.now() ? Math.min(until, ctx.cardsDueAt) : until)
|
|
1086
1159
|
// A slow pass can outlast the wait while a child publishes; ingest that answer once more
|
|
1087
1160
|
// rather than returning idle and leaving its slot unused until the next call.
|
|
1088
1161
|
let lastPass = false
|
|
1089
1162
|
for (;;) {
|
|
1090
|
-
if (!lastPass) await gatherSlots(ctx, wake())
|
|
1163
|
+
if (!lastPass) await gatherSlots(ctx, wake(), Number(options['card-agents'] ?? 0))
|
|
1091
1164
|
const result = await ingestAndRefill(ctx, options)
|
|
1092
1165
|
const idle =
|
|
1093
1166
|
!result.ingested.length &&
|
|
1167
|
+
!result.lost.length &&
|
|
1094
1168
|
!result.packs.length &&
|
|
1095
1169
|
!result.paused &&
|
|
1096
1170
|
!result.continue &&
|
|
1097
|
-
|
|
1171
|
+
// A due batch with no slot to plan a child on keeps waiting instead of spinning.
|
|
1172
|
+
!(result.cardBatch.ready && result.cardBatch.plannedAgents > 0) &&
|
|
1098
1173
|
(result.next === 'wait-for-children' || result.next.startsWith('next-packs'))
|
|
1099
1174
|
if (!idle || lastPass) return result
|
|
1100
1175
|
if (Date.now() >= until) {
|
|
@@ -1109,19 +1184,83 @@ async function autoRefill(ctx, options) {
|
|
|
1109
1184
|
)
|
|
1110
1185
|
}
|
|
1111
1186
|
}
|
|
1112
|
-
/**
|
|
1113
|
-
async function gatherSlots(ctx, until) {
|
|
1114
|
-
let firstFree = null
|
|
1187
|
+
/** Return at the first free slot; card deadlines still bound the wait. */
|
|
1188
|
+
async function gatherSlots(ctx, until, cardAgents = 0) {
|
|
1115
1189
|
while (Date.now() < until) {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
if (
|
|
1119
|
-
|
|
1120
|
-
if (Date.now() - firstFree >= GATHER_MS) return
|
|
1121
|
-
}
|
|
1190
|
+
await reclaimLostPacks(ctx)
|
|
1191
|
+
const free = SLOTS - cardAgents - (await outstandingPacks(ctx)).length - (ctx.lost?.length ?? 0)
|
|
1192
|
+
if (ctx.lost?.length) return
|
|
1193
|
+
if (free > 0) return
|
|
1122
1194
|
await new Promise((done) => setTimeout(done, Math.max(1, Math.min(1_000, until - Date.now()))))
|
|
1123
1195
|
}
|
|
1124
1196
|
}
|
|
1197
|
+
/** Fence lost task identities before releasing their batch reservations; resume after a crash. */
|
|
1198
|
+
async function reclaimLostPacks(ctx) {
|
|
1199
|
+
ctx.lost ??= []
|
|
1200
|
+
// Lease ages are wall-clock. After a gap no helper observed (a suspended machine, a parent that
|
|
1201
|
+
// stopped calling), every live lease restarts instead of all being lost at once.
|
|
1202
|
+
const now = Date.now()
|
|
1203
|
+
const gap = ctx.state.leaseCheckAt != null && now - ctx.state.leaseCheckAt > LOST_PACK_TIMEOUT_MS
|
|
1204
|
+
if (gap) ctx.state.leaseRearmedAt = now
|
|
1205
|
+
if (gap || ctx.state.leaseCheckAt == null || now - ctx.state.leaseCheckAt > 5_000) {
|
|
1206
|
+
ctx.state.leaseCheckAt = now
|
|
1207
|
+
await saveState(ctx)
|
|
1208
|
+
}
|
|
1209
|
+
for (const pack of await readAll(join(ctx.run, 'packs'))) {
|
|
1210
|
+
if (pack.state !== 'lost') {
|
|
1211
|
+
if (
|
|
1212
|
+
pack.state !== 'open' ||
|
|
1213
|
+
pack.repair ||
|
|
1214
|
+
!pack.units.some(({ batchId }) => {
|
|
1215
|
+
const unit = ctx.units.get(batchId)
|
|
1216
|
+
return unit?.packId === pack.id && unit.state === 'dispatched'
|
|
1217
|
+
}) ||
|
|
1218
|
+
(await waiting(ctx, pack))
|
|
1219
|
+
)
|
|
1220
|
+
continue
|
|
1221
|
+
const ageMs = now - Date.parse(pack.retryAt ?? pack.dispatchedAt)
|
|
1222
|
+
// Each earlier reclaim of one of its batches doubles the bound (at most 4x), so a slow
|
|
1223
|
+
// reader or a rate-limited account is not re-dispatched again and again.
|
|
1224
|
+
const reclaims = Math.max(0, ...pack.units.map((u) => ctx.state.reclaims?.[u.batchId] ?? 0))
|
|
1225
|
+
const bound = LOST_PACK_TIMEOUT_MS * 2 ** Math.min(reclaims, 2)
|
|
1226
|
+
const since = Math.min(ageMs, now - (ctx.state.leaseRearmedAt ?? 0))
|
|
1227
|
+
if (!(since >= bound)) continue
|
|
1228
|
+
// A retained original draft cannot keep a later retry child alive.
|
|
1229
|
+
const task = pack.retryTask ?? pack.task
|
|
1230
|
+
try {
|
|
1231
|
+
const draft = await stat(task.replace(/\.md$/u, '.draft.json'))
|
|
1232
|
+
if (now - draft.mtimeMs < DRAFT_HEARTBEAT_TIMEOUT_MS) continue
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
if (error.code !== 'ENOENT') throw error
|
|
1235
|
+
}
|
|
1236
|
+
pack.state = 'lost'
|
|
1237
|
+
pack.lostAt = new Date().toISOString()
|
|
1238
|
+
pack.lostAgeMs = ageMs
|
|
1239
|
+
await savePack(ctx, pack)
|
|
1240
|
+
}
|
|
1241
|
+
if (!pack.lostAcknowledgedAt) {
|
|
1242
|
+
if (!ctx.lost.some((item) => item.packId === pack.id))
|
|
1243
|
+
ctx.lost.push({ packId: pack.id, ageMs: pack.lostAgeMs })
|
|
1244
|
+
continue
|
|
1245
|
+
}
|
|
1246
|
+
for (const { batchId } of pack.units) {
|
|
1247
|
+
const unit = ctx.units.get(batchId)
|
|
1248
|
+
if (unit?.packId !== pack.id || unit.state !== 'dispatched') continue
|
|
1249
|
+
const batch = await loadBatch(ctx, batchId)
|
|
1250
|
+
if (!batch) fail('CORRUPTED_STATE')
|
|
1251
|
+
if (!ctx.state.queue.some((item) => item.batchId === batchId)) {
|
|
1252
|
+
ctx.state.queue.push(queueItem(batch))
|
|
1253
|
+
ctx.state.reclaims = {
|
|
1254
|
+
...ctx.state.reclaims,
|
|
1255
|
+
[batchId]: (ctx.state.reclaims?.[batchId] ?? 0) + 1,
|
|
1256
|
+
}
|
|
1257
|
+
await saveState(ctx)
|
|
1258
|
+
}
|
|
1259
|
+
await rm(unitPath(ctx, batchId), { force: true })
|
|
1260
|
+
ctx.units.delete(batchId)
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1125
1264
|
async function answerPublished(ctx) {
|
|
1126
1265
|
for (const pack of await readAll(join(ctx.run, 'packs')))
|
|
1127
1266
|
if (await waiting(ctx, pack)) return true
|
|
@@ -1451,8 +1590,51 @@ async function progressOf(ctx) {
|
|
|
1451
1590
|
blockedWorks: blockedWorks(view),
|
|
1452
1591
|
}
|
|
1453
1592
|
}
|
|
1593
|
+
function pendingPreparation(view, blocked = []) {
|
|
1594
|
+
return view.works
|
|
1595
|
+
.filter(
|
|
1596
|
+
(work) =>
|
|
1597
|
+
work.access !== 'drop' &&
|
|
1598
|
+
work.rootSessionIds.length > 0 &&
|
|
1599
|
+
work.status === 'pending' &&
|
|
1600
|
+
(work.totalBatches === 0 || blocked.includes(`${work.id}:${work.inputRevision}`)),
|
|
1601
|
+
)
|
|
1602
|
+
.map((work) => `${work.id}:${work.inputRevision}:${work.revision}`)
|
|
1603
|
+
.sort()
|
|
1604
|
+
.join('|')
|
|
1605
|
+
}
|
|
1454
1606
|
async function suggest(ctx, readOnly = false) {
|
|
1455
1607
|
if (paused(ctx)) return 'paused'
|
|
1608
|
+
// Retry changes revision but not inputRevision. Check pending input before the reading queue
|
|
1609
|
+
// drains, once per transition (and once after the owned prepare completes).
|
|
1610
|
+
const pending = pendingPreparation(ctx.view, ctx.state.preparationBlocked)
|
|
1611
|
+
if (!readOnly && ctx.state.preparationBusyPending) {
|
|
1612
|
+
ctx.state.preparationPending = pending || null
|
|
1613
|
+
ctx.state.preparationBusyPending = false
|
|
1614
|
+
await saveState(ctx)
|
|
1615
|
+
}
|
|
1616
|
+
// A live owned prepare admits newly pending Works through its read drain; its exit resets
|
|
1617
|
+
// preparationPending, so anything it left pending is suggested then.
|
|
1618
|
+
const ownedPrepareLive = ctx.state.preparationExitGeneration !== ctx.state.preparationGeneration
|
|
1619
|
+
if (
|
|
1620
|
+
ctx.state.preparationEverStarted &&
|
|
1621
|
+
!ownedPrepareLive &&
|
|
1622
|
+
pending &&
|
|
1623
|
+
pending
|
|
1624
|
+
.split('|')
|
|
1625
|
+
.some((item) => !(ctx.state.preparationPending ?? '').split('|').includes(item)) &&
|
|
1626
|
+
!ctx.state.preparationFailed
|
|
1627
|
+
) {
|
|
1628
|
+
if (!readOnly) {
|
|
1629
|
+
ctx.state.preparationPending = pending
|
|
1630
|
+
await saveState(ctx)
|
|
1631
|
+
}
|
|
1632
|
+
return 'journey.prepare'
|
|
1633
|
+
}
|
|
1634
|
+
if (!readOnly && pending !== (ctx.state.preparationPending ?? '')) {
|
|
1635
|
+
ctx.state.preparationPending = pending || null
|
|
1636
|
+
await saveState(ctx)
|
|
1637
|
+
}
|
|
1456
1638
|
const packs = await readAll(join(ctx.run, 'packs'))
|
|
1457
1639
|
for (const pack of packs) if (await waiting(ctx, pack)) return 'ingest'
|
|
1458
1640
|
if (packs.some((pack) => pack.repair)) return 'repair-answer'
|
|
@@ -1553,14 +1735,33 @@ async function status(ctx) {
|
|
|
1553
1735
|
async function prepareOutcome(ctx, options) {
|
|
1554
1736
|
const { result, code } = options
|
|
1555
1737
|
if (
|
|
1556
|
-
!['busy', 'started', 'failed'].includes(result) ||
|
|
1738
|
+
!['busy', 'started', 'completed', 'failed'].includes(result) ||
|
|
1557
1739
|
(result === 'busy' && code !== 'DISCOVERY_BUSY') ||
|
|
1558
|
-
(
|
|
1740
|
+
(['started', 'completed'].includes(result) && code !== undefined) ||
|
|
1559
1741
|
(result === 'failed' && (!/^[A-Z_]{3,80}$/u.test(code ?? '') || code === 'DISCOVERY_BUSY'))
|
|
1560
1742
|
)
|
|
1561
1743
|
fail('INVALID_ARGUMENTS')
|
|
1562
|
-
ctx.state.preparationStarted = result === 'started'
|
|
1744
|
+
if (result !== 'completed') ctx.state.preparationStarted = result === 'started'
|
|
1745
|
+
if (result === 'started') {
|
|
1746
|
+
ctx.state.preparationPending = pendingPreparation(
|
|
1747
|
+
await call(ctx, 'journey.get', { journeyId: ctx.journeyId }),
|
|
1748
|
+
ctx.state.preparationBlocked,
|
|
1749
|
+
)
|
|
1750
|
+
ctx.state.preparationEverStarted = true
|
|
1751
|
+
ctx.state.preparationBusyPending = false
|
|
1752
|
+
ctx.state.preparationGeneration = (ctx.state.preparationGeneration ?? 0) + 1
|
|
1753
|
+
}
|
|
1754
|
+
if (
|
|
1755
|
+
result === 'completed' &&
|
|
1756
|
+
ctx.state.preparationEverStarted &&
|
|
1757
|
+
ctx.state.preparationExitGeneration !== ctx.state.preparationGeneration
|
|
1758
|
+
) {
|
|
1759
|
+
ctx.state.preparationExitGeneration = ctx.state.preparationGeneration
|
|
1760
|
+
ctx.state.preparationBusyPending = false
|
|
1761
|
+
ctx.state.preparationPending = null
|
|
1762
|
+
}
|
|
1563
1763
|
if (result === 'busy') {
|
|
1764
|
+
ctx.state.preparationBusyPending = true
|
|
1564
1765
|
ctx.state.preparationRestarts = 0
|
|
1565
1766
|
ctx.state.preparationFailed = false
|
|
1566
1767
|
}
|
|
@@ -1639,6 +1840,9 @@ export async function main(argv) {
|
|
|
1639
1840
|
run: { type: 'string' },
|
|
1640
1841
|
journey: { type: 'string' },
|
|
1641
1842
|
agents: { type: 'string' },
|
|
1843
|
+
'card-agents': { type: 'string' },
|
|
1844
|
+
'ack-lost': { type: 'string' },
|
|
1845
|
+
'planned-card-agents': { type: 'string' },
|
|
1642
1846
|
model: { type: 'string' },
|
|
1643
1847
|
effort: { type: 'string' },
|
|
1644
1848
|
input: { type: 'string' },
|
|
@@ -1656,6 +1860,23 @@ export async function main(argv) {
|
|
|
1656
1860
|
const minimum = command === 'ingest' ? 0 : 1
|
|
1657
1861
|
if (!Number.isInteger(agents) || agents < minimum || agents > SLOTS) fail('INVALID_ARGUMENTS')
|
|
1658
1862
|
}
|
|
1863
|
+
const cardAgents = Number(values['card-agents'] ?? 0)
|
|
1864
|
+
if (
|
|
1865
|
+
!Number.isInteger(cardAgents) ||
|
|
1866
|
+
cardAgents < 0 ||
|
|
1867
|
+
cardAgents > SLOTS ||
|
|
1868
|
+
(values['card-agents'] !== undefined && !auto)
|
|
1869
|
+
)
|
|
1870
|
+
fail('INVALID_ARGUMENTS')
|
|
1871
|
+
const plannedCardAgents = Number(values['planned-card-agents'] ?? 1)
|
|
1872
|
+
if (
|
|
1873
|
+
!Number.isInteger(plannedCardAgents) ||
|
|
1874
|
+
plannedCardAgents < 1 ||
|
|
1875
|
+
plannedCardAgents > SLOTS ||
|
|
1876
|
+
(values['planned-card-agents'] !== undefined && !auto) ||
|
|
1877
|
+
(values['ack-lost'] !== undefined && (command !== 'ingest' || positionals[1]))
|
|
1878
|
+
)
|
|
1879
|
+
fail('INVALID_ARGUMENTS')
|
|
1659
1880
|
const waitMs = values['wait-ms'] === undefined ? 0 : Number(values['wait-ms'])
|
|
1660
1881
|
if (!(Number.isInteger(waitMs) && waitMs >= 0) || (values['wait-ms'] !== undefined && !auto))
|
|
1661
1882
|
fail('INVALID_ARGUMENTS')
|
|
@@ -1680,9 +1901,44 @@ export async function main(argv) {
|
|
|
1680
1901
|
fail('INVALID_ARGUMENTS')
|
|
1681
1902
|
const budget = values['budget-ms'] === undefined ? undefined : Number(values['budget-ms'])
|
|
1682
1903
|
if (budget !== undefined && !(Number.isInteger(budget) && budget >= 0)) fail('INVALID_ARGUMENTS')
|
|
1904
|
+
const started = Date.now()
|
|
1683
1905
|
const ctx = await openRun(values.run, budget)
|
|
1684
1906
|
try {
|
|
1685
|
-
|
|
1907
|
+
if (values['ack-lost'] !== undefined) {
|
|
1908
|
+
const ids = values['ack-lost'].split(',')
|
|
1909
|
+
if (!ids.length || ids.some((id) => !/^[a-zA-Z0-9-]+$/u.test(id))) fail('INVALID_ARGUMENTS')
|
|
1910
|
+
const packs = await readAll(join(ctx.run, 'packs'))
|
|
1911
|
+
const acknowledged = ids.map((id) => packs.find((pack) => pack.id === id))
|
|
1912
|
+
if (acknowledged.some((pack) => !pack || pack.state !== 'lost')) fail('UNKNOWN_PACK')
|
|
1913
|
+
for (const pack of acknowledged) {
|
|
1914
|
+
pack.lostAcknowledgedAt ??= new Date().toISOString()
|
|
1915
|
+
await savePack(ctx, pack)
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
const result = await handler(ctx)
|
|
1919
|
+
if (['ingest', 'next-packs'].includes(command) && ctx.view) {
|
|
1920
|
+
try {
|
|
1921
|
+
await scanTiming(
|
|
1922
|
+
ctx,
|
|
1923
|
+
command === 'ingest' ? cardAgents : undefined,
|
|
1924
|
+
command === 'ingest' ? started : undefined,
|
|
1925
|
+
)
|
|
1926
|
+
} catch {
|
|
1927
|
+
// Observability must never turn a committed ingest into a retryable failure.
|
|
1928
|
+
console.error('SCAN_TIMING_FAILED')
|
|
1929
|
+
try {
|
|
1930
|
+
await timing(ctx, {
|
|
1931
|
+
phase: 'local',
|
|
1932
|
+
operation: 'scanTiming',
|
|
1933
|
+
started: new Date(started),
|
|
1934
|
+
outcome: 'SCAN_TIMING_FAILED',
|
|
1935
|
+
})
|
|
1936
|
+
} catch {
|
|
1937
|
+
console.error('SCAN_TIMING_DIAGNOSTIC_FAILED')
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return { ...result, agentProfile: ctx.config.agentProfile }
|
|
1686
1942
|
} finally {
|
|
1687
1943
|
await Promise.all((ctx.streams ?? []).map((item) => item.close()))
|
|
1688
1944
|
await ctx.release()
|
package/LICENSE
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
PROPRIETARY SOFTWARE — ALL RIGHTS RESERVED
|
|
2
|
-
|
|
3
|
-
This repository is not licensed as open-source software. Except for permissions
|
|
4
|
-
expressly granted by the applicable rights holders in a separate written agreement,
|
|
5
|
-
no permission is granted to use, copy, modify, distribute, sublicense, or sell this
|
|
6
|
-
software or substantial portions of it. Public access to source code does not itself
|
|
7
|
-
grant such permission. Rights provided by applicable law or the hosting platform's
|
|
8
|
-
terms are not limited by this notice.
|
|
9
|
-
|
|
10
|
-
Third-party components remain subject to their respective licenses and notices.
|
|
11
|
-
This notice does not replace or restrict permissions granted by those licenses.
|