@gitroll/tacitt-cli 0.6.7 → 0.6.9
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-CODlN6zl.mjs} +1 -1
- package/dist/agent-setup-worker.mjs +4 -4
- package/dist/cli.mjs +5 -5
- package/dist/codex-title-worker.mjs +1 -1
- package/dist/configured-preview-worker.mjs +4 -4
- package/dist/configured-session-worker.mjs +4 -4
- package/dist/{heap-DsE74_-n.mjs → heap-Dt3-d27v.mjs} +2 -2
- package/dist/inspection-worker.mjs +3 -3
- package/dist/{job-CeSf9UCC.mjs → job-BQXssW46.mjs} +3 -3
- package/dist/{model-f_QnDyyb.mjs → model-CSx2dP_P.mjs} +1 -1
- package/dist/operation-executor.mjs +2 -2
- package/dist/preview-worker.mjs +1 -1
- package/dist/{query-Bb72w2DX.mjs → query-CfhcS4gd.mjs} +194 -134
- package/dist/{run-RA_8zZsL.mjs → run-xlOaKCk8.mjs} +2 -2
- package/dist/{src-BBjnljUs.mjs → src-BgsEnMsy.mjs} +3 -3
- package/dist/{src-d7n58sbH.mjs → src-DzKuzGVH.mjs} +183 -147
- package/dist/{src-p_uJNjV3.mjs → src-V9GHLZQs.mjs} +1 -1
- package/dist/store-database-worker.mjs +1 -1
- package/dist/web/assets/{index-B6kJ6XXF.js → index-9UfCT_2C.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/skills/tacitt/SKILL.md +3 -3
- package/skills/tacitt/references/commands.md +50 -11
- package/skills/tacitt/references/journey.md +1 -1
- package/skills/tacitt/references/parallel-review.md +21 -0
- package/skills/tacitt/scripts/INDEX.md +39 -36
- 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.mjs +15 -0
- package/skills/tacitt/scripts/review.mjs +280 -36
- package/LICENSE +0 -11
|
@@ -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
|
|
@@ -301,7 +300,17 @@ async function observe(ctx, readOnly = false) {
|
|
|
301
300
|
ctx.state.preparationFailed = false
|
|
302
301
|
if (ctx.state.reader) ctx.state.reader.waitingSince = null
|
|
303
302
|
}
|
|
304
|
-
if (!readOnly)
|
|
303
|
+
if (!readOnly) {
|
|
304
|
+
ctx.state.completedWorks = completedWorks
|
|
305
|
+
ctx.state.preparationBlocked = [
|
|
306
|
+
...new Set([
|
|
307
|
+
...(ctx.state.preparationBlocked ?? []),
|
|
308
|
+
...view.works
|
|
309
|
+
.filter((work) => work.status === 'blocked')
|
|
310
|
+
.map((work) => `${work.id}:${work.inputRevision}`),
|
|
311
|
+
]),
|
|
312
|
+
]
|
|
313
|
+
}
|
|
305
314
|
const works = Object.fromEntries(
|
|
306
315
|
view.works.map((work) => [work.id, { inputRevision: work.inputRevision, access: work.access }]),
|
|
307
316
|
)
|
|
@@ -587,7 +596,7 @@ const withAnswer = (task, text) => {
|
|
|
587
596
|
// Each pack names its own draft so concurrent children never share a scratch file.
|
|
588
597
|
const draft = resolve(task.replace(/\.md$/u, '.draft.json'))
|
|
589
598
|
const command = `${shellQuote(process.execPath)} ${shellQuote(resolve(task.replace(/\.md$/u, '.publish.mjs')))} ${shellQuote(draft)}`
|
|
590
|
-
return `${text}\n\n## Your answer\n\
|
|
599
|
+
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
600
|
}
|
|
592
601
|
/** Validate a complete task answer before the publisher replaces any prior answer. */
|
|
593
602
|
export function validatePrivacyAnswer(answer, unitRefs) {
|
|
@@ -656,7 +665,7 @@ function packPrompt(task, text) {
|
|
|
656
665
|
const reads = plan.map(({ offset, limit }) => `(${offset},${limit})`).join(' ')
|
|
657
666
|
// In the prompt itself, not a start file: given the plan indirectly, fast models read the
|
|
658
667
|
// 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.`
|
|
668
|
+
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
669
|
}
|
|
661
670
|
/** Writes a task and its publisher and returns the agent's prompt. */
|
|
662
671
|
export async function writePackTask(task, text, unitRefs, sourceBatches = []) {
|
|
@@ -908,6 +917,8 @@ async function finishPack(ctx, meta) {
|
|
|
908
917
|
ctx.state.queue.unshift(...requeue)
|
|
909
918
|
meta.units = kept
|
|
910
919
|
meta.state = kept.length ? 'open' : 'empty'
|
|
920
|
+
// The lease starts when the task is handed out, also for a pack resumed from dispatching.
|
|
921
|
+
meta.dispatchedAt = new Date().toISOString()
|
|
911
922
|
meta.codePoints = batches.reduce(
|
|
912
923
|
(total, item) => total + item.unit.coreCodePoints + item.unit.contextCodePoints,
|
|
913
924
|
0,
|
|
@@ -958,7 +969,9 @@ async function waiting(ctx, pack) {
|
|
|
958
969
|
/** Every open pack whose agent wrote its answer, or whose saved answer is not recorded yet. */
|
|
959
970
|
async function ingestAll(ctx) {
|
|
960
971
|
const changes = await observe(ctx)
|
|
972
|
+
await reclaimLostPacks(ctx)
|
|
961
973
|
const ingested = []
|
|
974
|
+
const freedSlots = new Set((ctx.lost ?? []).map((pack) => pack.packId))
|
|
962
975
|
let stopped = false
|
|
963
976
|
const terms = { added: 0, kept: 0, unlocated: 0 }
|
|
964
977
|
for (const pack of await readAll(join(ctx.run, 'packs'))) {
|
|
@@ -981,6 +994,13 @@ async function ingestAll(ctx) {
|
|
|
981
994
|
})
|
|
982
995
|
continue
|
|
983
996
|
}
|
|
997
|
+
if (
|
|
998
|
+
!pack.units.some(({ batchId }) => {
|
|
999
|
+
const unit = ctx.units.get(batchId)
|
|
1000
|
+
return unit?.packId === pack.id && unit.state === 'dispatched'
|
|
1001
|
+
})
|
|
1002
|
+
)
|
|
1003
|
+
freedSlots.add(pack.id)
|
|
984
1004
|
for (const key of Object.keys(terms)) terms[key] += result.terms[key]
|
|
985
1005
|
ingested.push({
|
|
986
1006
|
packId: pack.id,
|
|
@@ -1001,6 +1021,8 @@ async function ingestAll(ctx) {
|
|
|
1001
1021
|
if (!paused(ctx) && !stopped && !overBudget(ctx)) await readNext(ctx)
|
|
1002
1022
|
return {
|
|
1003
1023
|
changes,
|
|
1024
|
+
lost: ctx.lost ?? [],
|
|
1025
|
+
freedSlots: [...freedSlots],
|
|
1004
1026
|
ingested,
|
|
1005
1027
|
terms,
|
|
1006
1028
|
...(await progressOf(ctx)),
|
|
@@ -1009,6 +1031,42 @@ async function ingestAll(ctx) {
|
|
|
1009
1031
|
next: stopped ? 'ingest' : await suggest(ctx),
|
|
1010
1032
|
}
|
|
1011
1033
|
}
|
|
1034
|
+
async function scanTiming(ctx, cardAgents, ingestStartedAt) {
|
|
1035
|
+
const receipt = await readJson(join(ctx.run, 'scan-timing.json'), null)
|
|
1036
|
+
if (!receipt || receipt.completedAt) return
|
|
1037
|
+
const packs = []
|
|
1038
|
+
for (const pack of await readAll(join(ctx.run, 'packs'))) {
|
|
1039
|
+
let publishedAt = null
|
|
1040
|
+
try {
|
|
1041
|
+
publishedAt = (await stat(answerPath(pack.retryTask ?? pack.task))).mtimeMs
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
if (error.code !== 'ENOENT') throw error
|
|
1044
|
+
}
|
|
1045
|
+
const active =
|
|
1046
|
+
pack.state === 'open' &&
|
|
1047
|
+
pack.units.some(({ batchId }) => {
|
|
1048
|
+
const unit = ctx.units.get(batchId)
|
|
1049
|
+
return unit?.packId === pack.id && unit.state === 'dispatched'
|
|
1050
|
+
})
|
|
1051
|
+
if ((active || publishedAt !== null) && Number.isFinite(Date.parse(pack.dispatchedAt)))
|
|
1052
|
+
packs.push({ id: pack.id, dispatchedAt: pack.dispatchedAt, publishedAt })
|
|
1053
|
+
}
|
|
1054
|
+
await observeScanTiming(ctx.run, {
|
|
1055
|
+
at: Date.now(),
|
|
1056
|
+
...(ingestStartedAt === undefined ? {} : { ingestStartedAt }),
|
|
1057
|
+
packs,
|
|
1058
|
+
activeCardChildren: cardAgents ?? receipt.measurements?.last?.activeCardChildren ?? 0,
|
|
1059
|
+
queuedBatches: ctx.state.queue.length,
|
|
1060
|
+
unreadBatches: ctx.state.reader?.remaining ?? 0,
|
|
1061
|
+
waiting: ctx.state.reader?.waiting,
|
|
1062
|
+
pendingWorkIds: ctx.view.works
|
|
1063
|
+
.filter(
|
|
1064
|
+
(work) =>
|
|
1065
|
+
work.access !== 'drop' && work.rootSessionIds.length > 0 && work.status === 'pending',
|
|
1066
|
+
)
|
|
1067
|
+
.map((work) => work.id),
|
|
1068
|
+
})
|
|
1069
|
+
}
|
|
1012
1070
|
/** Refill only confirmed free native slots, after exact ingestion has settled. */
|
|
1013
1071
|
async function ingestAndRefill(ctx, options) {
|
|
1014
1072
|
const auto = options.agents === 'auto'
|
|
@@ -1017,16 +1075,18 @@ async function ingestAndRefill(ctx, options) {
|
|
|
1017
1075
|
fail('INVALID_ARGUMENTS')
|
|
1018
1076
|
const result = await ingestAll(ctx)
|
|
1019
1077
|
const outstanding = await outstandingPacks(ctx)
|
|
1020
|
-
const
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1078
|
+
const lostSlots = result.lost.length
|
|
1079
|
+
const available = Math.max(
|
|
1080
|
+
0,
|
|
1081
|
+
SLOTS - outstanding.length - Number(options['card-agents'] ?? 0) - lostSlots,
|
|
1082
|
+
)
|
|
1083
|
+
const plannedAgents = result.cardBatch.ready
|
|
1084
|
+
? Math.min(available, Number(options['planned-card-agents'] ?? 1))
|
|
1085
|
+
: 0
|
|
1086
|
+
result.cardBatch.plannedAgents = plannedAgents
|
|
1087
|
+
// An explicit count never takes live leases past the sixteen slots (ADR 0179).
|
|
1088
|
+
const agents = auto ? available - plannedAgents : Math.min(requested, available - plannedAgents)
|
|
1089
|
+
const slots = auto ? { freeSlots: agents } : {}
|
|
1030
1090
|
const unsettled = result.ingested.some(
|
|
1031
1091
|
(item) =>
|
|
1032
1092
|
item.error ||
|
|
@@ -1082,19 +1142,22 @@ async function autoRefill(ctx, options) {
|
|
|
1082
1142
|
// ADR 0234: a card batch can come due on time alone; neither gathering slots nor waiting for a
|
|
1083
1143
|
// child holds it back past that time.
|
|
1084
1144
|
await progressOf(ctx)
|
|
1085
|
-
|
|
1145
|
+
// A batch already due wakes nothing more: it waits for a free slot like any refill.
|
|
1146
|
+
const wake = () => (ctx.cardsDueAt > Date.now() ? Math.min(until, ctx.cardsDueAt) : until)
|
|
1086
1147
|
// A slow pass can outlast the wait while a child publishes; ingest that answer once more
|
|
1087
1148
|
// rather than returning idle and leaving its slot unused until the next call.
|
|
1088
1149
|
let lastPass = false
|
|
1089
1150
|
for (;;) {
|
|
1090
|
-
if (!lastPass) await gatherSlots(ctx, wake())
|
|
1151
|
+
if (!lastPass) await gatherSlots(ctx, wake(), Number(options['card-agents'] ?? 0))
|
|
1091
1152
|
const result = await ingestAndRefill(ctx, options)
|
|
1092
1153
|
const idle =
|
|
1093
1154
|
!result.ingested.length &&
|
|
1155
|
+
!result.lost.length &&
|
|
1094
1156
|
!result.packs.length &&
|
|
1095
1157
|
!result.paused &&
|
|
1096
1158
|
!result.continue &&
|
|
1097
|
-
|
|
1159
|
+
// A due batch with no slot to plan a child on keeps waiting instead of spinning.
|
|
1160
|
+
!(result.cardBatch.ready && result.cardBatch.plannedAgents > 0) &&
|
|
1098
1161
|
(result.next === 'wait-for-children' || result.next.startsWith('next-packs'))
|
|
1099
1162
|
if (!idle || lastPass) return result
|
|
1100
1163
|
if (Date.now() >= until) {
|
|
@@ -1109,19 +1172,83 @@ async function autoRefill(ctx, options) {
|
|
|
1109
1172
|
)
|
|
1110
1173
|
}
|
|
1111
1174
|
}
|
|
1112
|
-
/**
|
|
1113
|
-
async function gatherSlots(ctx, until) {
|
|
1114
|
-
let firstFree = null
|
|
1175
|
+
/** Return at the first free slot; card deadlines still bound the wait. */
|
|
1176
|
+
async function gatherSlots(ctx, until, cardAgents = 0) {
|
|
1115
1177
|
while (Date.now() < until) {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
if (
|
|
1119
|
-
|
|
1120
|
-
if (Date.now() - firstFree >= GATHER_MS) return
|
|
1121
|
-
}
|
|
1178
|
+
await reclaimLostPacks(ctx)
|
|
1179
|
+
const free = SLOTS - cardAgents - (await outstandingPacks(ctx)).length - (ctx.lost?.length ?? 0)
|
|
1180
|
+
if (ctx.lost?.length) return
|
|
1181
|
+
if (free > 0) return
|
|
1122
1182
|
await new Promise((done) => setTimeout(done, Math.max(1, Math.min(1_000, until - Date.now()))))
|
|
1123
1183
|
}
|
|
1124
1184
|
}
|
|
1185
|
+
/** Fence lost task identities before releasing their batch reservations; resume after a crash. */
|
|
1186
|
+
async function reclaimLostPacks(ctx) {
|
|
1187
|
+
ctx.lost ??= []
|
|
1188
|
+
// Lease ages are wall-clock. After a gap no helper observed (a suspended machine, a parent that
|
|
1189
|
+
// stopped calling), every live lease restarts instead of all being lost at once.
|
|
1190
|
+
const now = Date.now()
|
|
1191
|
+
const gap = ctx.state.leaseCheckAt != null && now - ctx.state.leaseCheckAt > LOST_PACK_TIMEOUT_MS
|
|
1192
|
+
if (gap) ctx.state.leaseRearmedAt = now
|
|
1193
|
+
if (gap || ctx.state.leaseCheckAt == null || now - ctx.state.leaseCheckAt > 5_000) {
|
|
1194
|
+
ctx.state.leaseCheckAt = now
|
|
1195
|
+
await saveState(ctx)
|
|
1196
|
+
}
|
|
1197
|
+
for (const pack of await readAll(join(ctx.run, 'packs'))) {
|
|
1198
|
+
if (pack.state !== 'lost') {
|
|
1199
|
+
if (
|
|
1200
|
+
pack.state !== 'open' ||
|
|
1201
|
+
pack.repair ||
|
|
1202
|
+
!pack.units.some(({ batchId }) => {
|
|
1203
|
+
const unit = ctx.units.get(batchId)
|
|
1204
|
+
return unit?.packId === pack.id && unit.state === 'dispatched'
|
|
1205
|
+
}) ||
|
|
1206
|
+
(await waiting(ctx, pack))
|
|
1207
|
+
)
|
|
1208
|
+
continue
|
|
1209
|
+
const ageMs = now - Date.parse(pack.retryAt ?? pack.dispatchedAt)
|
|
1210
|
+
// Each earlier reclaim of one of its batches doubles the bound (at most 4x), so a slow
|
|
1211
|
+
// reader or a rate-limited account is not re-dispatched again and again.
|
|
1212
|
+
const reclaims = Math.max(0, ...pack.units.map((u) => ctx.state.reclaims?.[u.batchId] ?? 0))
|
|
1213
|
+
const bound = LOST_PACK_TIMEOUT_MS * 2 ** Math.min(reclaims, 2)
|
|
1214
|
+
const since = Math.min(ageMs, now - (ctx.state.leaseRearmedAt ?? 0))
|
|
1215
|
+
if (!(since >= bound)) continue
|
|
1216
|
+
// A retained original draft cannot keep a later retry child alive.
|
|
1217
|
+
const task = pack.retryTask ?? pack.task
|
|
1218
|
+
try {
|
|
1219
|
+
const draft = await stat(task.replace(/\.md$/u, '.draft.json'))
|
|
1220
|
+
if (now - draft.mtimeMs < DRAFT_HEARTBEAT_TIMEOUT_MS) continue
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
if (error.code !== 'ENOENT') throw error
|
|
1223
|
+
}
|
|
1224
|
+
pack.state = 'lost'
|
|
1225
|
+
pack.lostAt = new Date().toISOString()
|
|
1226
|
+
pack.lostAgeMs = ageMs
|
|
1227
|
+
await savePack(ctx, pack)
|
|
1228
|
+
}
|
|
1229
|
+
if (!pack.lostAcknowledgedAt) {
|
|
1230
|
+
if (!ctx.lost.some((item) => item.packId === pack.id))
|
|
1231
|
+
ctx.lost.push({ packId: pack.id, ageMs: pack.lostAgeMs })
|
|
1232
|
+
continue
|
|
1233
|
+
}
|
|
1234
|
+
for (const { batchId } of pack.units) {
|
|
1235
|
+
const unit = ctx.units.get(batchId)
|
|
1236
|
+
if (unit?.packId !== pack.id || unit.state !== 'dispatched') continue
|
|
1237
|
+
const batch = await loadBatch(ctx, batchId)
|
|
1238
|
+
if (!batch) fail('CORRUPTED_STATE')
|
|
1239
|
+
if (!ctx.state.queue.some((item) => item.batchId === batchId)) {
|
|
1240
|
+
ctx.state.queue.push(queueItem(batch))
|
|
1241
|
+
ctx.state.reclaims = {
|
|
1242
|
+
...ctx.state.reclaims,
|
|
1243
|
+
[batchId]: (ctx.state.reclaims?.[batchId] ?? 0) + 1,
|
|
1244
|
+
}
|
|
1245
|
+
await saveState(ctx)
|
|
1246
|
+
}
|
|
1247
|
+
await rm(unitPath(ctx, batchId), { force: true })
|
|
1248
|
+
ctx.units.delete(batchId)
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1125
1252
|
async function answerPublished(ctx) {
|
|
1126
1253
|
for (const pack of await readAll(join(ctx.run, 'packs')))
|
|
1127
1254
|
if (await waiting(ctx, pack)) return true
|
|
@@ -1451,8 +1578,51 @@ async function progressOf(ctx) {
|
|
|
1451
1578
|
blockedWorks: blockedWorks(view),
|
|
1452
1579
|
}
|
|
1453
1580
|
}
|
|
1581
|
+
function pendingPreparation(view, blocked = []) {
|
|
1582
|
+
return view.works
|
|
1583
|
+
.filter(
|
|
1584
|
+
(work) =>
|
|
1585
|
+
work.access !== 'drop' &&
|
|
1586
|
+
work.rootSessionIds.length > 0 &&
|
|
1587
|
+
work.status === 'pending' &&
|
|
1588
|
+
(work.totalBatches === 0 || blocked.includes(`${work.id}:${work.inputRevision}`)),
|
|
1589
|
+
)
|
|
1590
|
+
.map((work) => `${work.id}:${work.inputRevision}:${work.revision}`)
|
|
1591
|
+
.sort()
|
|
1592
|
+
.join('|')
|
|
1593
|
+
}
|
|
1454
1594
|
async function suggest(ctx, readOnly = false) {
|
|
1455
1595
|
if (paused(ctx)) return 'paused'
|
|
1596
|
+
// Retry changes revision but not inputRevision. Check pending input before the reading queue
|
|
1597
|
+
// drains, once per transition (and once after the owned prepare completes).
|
|
1598
|
+
const pending = pendingPreparation(ctx.view, ctx.state.preparationBlocked)
|
|
1599
|
+
if (!readOnly && ctx.state.preparationBusyPending) {
|
|
1600
|
+
ctx.state.preparationPending = pending || null
|
|
1601
|
+
ctx.state.preparationBusyPending = false
|
|
1602
|
+
await saveState(ctx)
|
|
1603
|
+
}
|
|
1604
|
+
// A live owned prepare admits newly pending Works through its read drain; its exit resets
|
|
1605
|
+
// preparationPending, so anything it left pending is suggested then.
|
|
1606
|
+
const ownedPrepareLive = ctx.state.preparationExitGeneration !== ctx.state.preparationGeneration
|
|
1607
|
+
if (
|
|
1608
|
+
ctx.state.preparationEverStarted &&
|
|
1609
|
+
!ownedPrepareLive &&
|
|
1610
|
+
pending &&
|
|
1611
|
+
pending
|
|
1612
|
+
.split('|')
|
|
1613
|
+
.some((item) => !(ctx.state.preparationPending ?? '').split('|').includes(item)) &&
|
|
1614
|
+
!ctx.state.preparationFailed
|
|
1615
|
+
) {
|
|
1616
|
+
if (!readOnly) {
|
|
1617
|
+
ctx.state.preparationPending = pending
|
|
1618
|
+
await saveState(ctx)
|
|
1619
|
+
}
|
|
1620
|
+
return 'journey.prepare'
|
|
1621
|
+
}
|
|
1622
|
+
if (!readOnly && pending !== (ctx.state.preparationPending ?? '')) {
|
|
1623
|
+
ctx.state.preparationPending = pending || null
|
|
1624
|
+
await saveState(ctx)
|
|
1625
|
+
}
|
|
1456
1626
|
const packs = await readAll(join(ctx.run, 'packs'))
|
|
1457
1627
|
for (const pack of packs) if (await waiting(ctx, pack)) return 'ingest'
|
|
1458
1628
|
if (packs.some((pack) => pack.repair)) return 'repair-answer'
|
|
@@ -1553,14 +1723,33 @@ async function status(ctx) {
|
|
|
1553
1723
|
async function prepareOutcome(ctx, options) {
|
|
1554
1724
|
const { result, code } = options
|
|
1555
1725
|
if (
|
|
1556
|
-
!['busy', 'started', 'failed'].includes(result) ||
|
|
1726
|
+
!['busy', 'started', 'completed', 'failed'].includes(result) ||
|
|
1557
1727
|
(result === 'busy' && code !== 'DISCOVERY_BUSY') ||
|
|
1558
|
-
(
|
|
1728
|
+
(['started', 'completed'].includes(result) && code !== undefined) ||
|
|
1559
1729
|
(result === 'failed' && (!/^[A-Z_]{3,80}$/u.test(code ?? '') || code === 'DISCOVERY_BUSY'))
|
|
1560
1730
|
)
|
|
1561
1731
|
fail('INVALID_ARGUMENTS')
|
|
1562
|
-
ctx.state.preparationStarted = result === 'started'
|
|
1732
|
+
if (result !== 'completed') ctx.state.preparationStarted = result === 'started'
|
|
1733
|
+
if (result === 'started') {
|
|
1734
|
+
ctx.state.preparationPending = pendingPreparation(
|
|
1735
|
+
await call(ctx, 'journey.get', { journeyId: ctx.journeyId }),
|
|
1736
|
+
ctx.state.preparationBlocked,
|
|
1737
|
+
)
|
|
1738
|
+
ctx.state.preparationEverStarted = true
|
|
1739
|
+
ctx.state.preparationBusyPending = false
|
|
1740
|
+
ctx.state.preparationGeneration = (ctx.state.preparationGeneration ?? 0) + 1
|
|
1741
|
+
}
|
|
1742
|
+
if (
|
|
1743
|
+
result === 'completed' &&
|
|
1744
|
+
ctx.state.preparationEverStarted &&
|
|
1745
|
+
ctx.state.preparationExitGeneration !== ctx.state.preparationGeneration
|
|
1746
|
+
) {
|
|
1747
|
+
ctx.state.preparationExitGeneration = ctx.state.preparationGeneration
|
|
1748
|
+
ctx.state.preparationBusyPending = false
|
|
1749
|
+
ctx.state.preparationPending = null
|
|
1750
|
+
}
|
|
1563
1751
|
if (result === 'busy') {
|
|
1752
|
+
ctx.state.preparationBusyPending = true
|
|
1564
1753
|
ctx.state.preparationRestarts = 0
|
|
1565
1754
|
ctx.state.preparationFailed = false
|
|
1566
1755
|
}
|
|
@@ -1639,6 +1828,9 @@ export async function main(argv) {
|
|
|
1639
1828
|
run: { type: 'string' },
|
|
1640
1829
|
journey: { type: 'string' },
|
|
1641
1830
|
agents: { type: 'string' },
|
|
1831
|
+
'card-agents': { type: 'string' },
|
|
1832
|
+
'ack-lost': { type: 'string' },
|
|
1833
|
+
'planned-card-agents': { type: 'string' },
|
|
1642
1834
|
model: { type: 'string' },
|
|
1643
1835
|
effort: { type: 'string' },
|
|
1644
1836
|
input: { type: 'string' },
|
|
@@ -1656,6 +1848,23 @@ export async function main(argv) {
|
|
|
1656
1848
|
const minimum = command === 'ingest' ? 0 : 1
|
|
1657
1849
|
if (!Number.isInteger(agents) || agents < minimum || agents > SLOTS) fail('INVALID_ARGUMENTS')
|
|
1658
1850
|
}
|
|
1851
|
+
const cardAgents = Number(values['card-agents'] ?? 0)
|
|
1852
|
+
if (
|
|
1853
|
+
!Number.isInteger(cardAgents) ||
|
|
1854
|
+
cardAgents < 0 ||
|
|
1855
|
+
cardAgents > SLOTS ||
|
|
1856
|
+
(values['card-agents'] !== undefined && !auto)
|
|
1857
|
+
)
|
|
1858
|
+
fail('INVALID_ARGUMENTS')
|
|
1859
|
+
const plannedCardAgents = Number(values['planned-card-agents'] ?? 1)
|
|
1860
|
+
if (
|
|
1861
|
+
!Number.isInteger(plannedCardAgents) ||
|
|
1862
|
+
plannedCardAgents < 1 ||
|
|
1863
|
+
plannedCardAgents > SLOTS ||
|
|
1864
|
+
(values['planned-card-agents'] !== undefined && !auto) ||
|
|
1865
|
+
(values['ack-lost'] !== undefined && (command !== 'ingest' || positionals[1]))
|
|
1866
|
+
)
|
|
1867
|
+
fail('INVALID_ARGUMENTS')
|
|
1659
1868
|
const waitMs = values['wait-ms'] === undefined ? 0 : Number(values['wait-ms'])
|
|
1660
1869
|
if (!(Number.isInteger(waitMs) && waitMs >= 0) || (values['wait-ms'] !== undefined && !auto))
|
|
1661
1870
|
fail('INVALID_ARGUMENTS')
|
|
@@ -1680,9 +1889,44 @@ export async function main(argv) {
|
|
|
1680
1889
|
fail('INVALID_ARGUMENTS')
|
|
1681
1890
|
const budget = values['budget-ms'] === undefined ? undefined : Number(values['budget-ms'])
|
|
1682
1891
|
if (budget !== undefined && !(Number.isInteger(budget) && budget >= 0)) fail('INVALID_ARGUMENTS')
|
|
1892
|
+
const started = Date.now()
|
|
1683
1893
|
const ctx = await openRun(values.run, budget)
|
|
1684
1894
|
try {
|
|
1685
|
-
|
|
1895
|
+
if (values['ack-lost'] !== undefined) {
|
|
1896
|
+
const ids = values['ack-lost'].split(',')
|
|
1897
|
+
if (!ids.length || ids.some((id) => !/^[a-zA-Z0-9-]+$/u.test(id))) fail('INVALID_ARGUMENTS')
|
|
1898
|
+
const packs = await readAll(join(ctx.run, 'packs'))
|
|
1899
|
+
const acknowledged = ids.map((id) => packs.find((pack) => pack.id === id))
|
|
1900
|
+
if (acknowledged.some((pack) => !pack || pack.state !== 'lost')) fail('UNKNOWN_PACK')
|
|
1901
|
+
for (const pack of acknowledged) {
|
|
1902
|
+
pack.lostAcknowledgedAt ??= new Date().toISOString()
|
|
1903
|
+
await savePack(ctx, pack)
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
const result = await handler(ctx)
|
|
1907
|
+
if (['ingest', 'next-packs'].includes(command) && ctx.view) {
|
|
1908
|
+
try {
|
|
1909
|
+
await scanTiming(
|
|
1910
|
+
ctx,
|
|
1911
|
+
command === 'ingest' ? cardAgents : undefined,
|
|
1912
|
+
command === 'ingest' ? started : undefined,
|
|
1913
|
+
)
|
|
1914
|
+
} catch {
|
|
1915
|
+
// Observability must never turn a committed ingest into a retryable failure.
|
|
1916
|
+
console.error('SCAN_TIMING_FAILED')
|
|
1917
|
+
try {
|
|
1918
|
+
await timing(ctx, {
|
|
1919
|
+
phase: 'local',
|
|
1920
|
+
operation: 'scanTiming',
|
|
1921
|
+
started: new Date(started),
|
|
1922
|
+
outcome: 'SCAN_TIMING_FAILED',
|
|
1923
|
+
})
|
|
1924
|
+
} catch {
|
|
1925
|
+
console.error('SCAN_TIMING_DIAGNOSTIC_FAILED')
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return { ...result, agentProfile: ctx.config.agentProfile }
|
|
1686
1930
|
} finally {
|
|
1687
1931
|
await Promise.all((ctx.streams ?? []).map((item) => item.close()))
|
|
1688
1932
|
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.
|