sortie-dogs 0.9.11 → 0.9.12
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 +22 -1
- package/dist/plugin/continuation.d.ts +1 -1
- package/dist/plugin/continuation.js +19 -4
- package/dist/plugin/index.d.ts +5 -0
- package/dist/plugin/index.js +162 -89
- package/dist/plugin/run-metrics.d.ts +9 -1
- package/dist/plugin/run-metrics.js +66 -12
- package/dist/plugin/sortie-career.js +18 -14
- package/dist/plugin/sortie-debrief.d.ts +4 -0
- package/dist/plugin/sortie-debrief.js +22 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
|
|
|
22
22
|
|
|
23
23
|
Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [テスト実行](docs/testing.md) · [CLI testing](docs/cli-testing.md)
|
|
24
24
|
|
|
25
|
-
Release: [v0.9.
|
|
25
|
+
Release: [v0.9.12](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.9.12)
|
|
26
26
|
|
|
27
27
|
## Provisional quality–cost position
|
|
28
28
|
|
|
@@ -74,6 +74,15 @@ models are reserved for implementation, escalation, and independent review.
|
|
|
74
74
|
Writes stay scoped, and completion requires validation evidence. Every completed
|
|
75
75
|
run can return a concise Speed / Cost / Proof debrief.
|
|
76
76
|
|
|
77
|
+
### Use only as much harness as the task needs
|
|
78
|
+
|
|
79
|
+
Small changes can skip Scout and independent review when one worker and targeted
|
|
80
|
+
validation are sufficient. Larger work can be decomposed into multiple units;
|
|
81
|
+
units that are safely independent can use a Luna fabric DAG for bounded parallel
|
|
82
|
+
execution. Higher-risk candidates add independent review, while full-suite and
|
|
83
|
+
package verification are reserved for release work. Not every task pays the
|
|
84
|
+
cost of the heaviest workflow.
|
|
85
|
+
|
|
77
86
|
## Designed to coexist with OpenCode
|
|
78
87
|
|
|
79
88
|
Sortie-dogs adds a workflow to your existing setup rather than replacing it.
|
|
@@ -299,6 +308,18 @@ dog-coordinator: completion evidence accepted
|
|
|
299
308
|
progress; repeated batches remain bounded rather than becoming endless
|
|
300
309
|
delegation.
|
|
301
310
|
|
|
311
|
+
## Built to work on itself
|
|
312
|
+
|
|
313
|
+
Self-improvement keeps the same scoped manifests, worker ownership, validation,
|
|
314
|
+
and review gates as other work. A loaded plugin is not treated as hot-reloadable:
|
|
315
|
+
source changes are validated first, then packaged into an isolated `_testenv`
|
|
316
|
+
fixture and exercised through the real OpenCode CLI. Continuation and compaction
|
|
317
|
+
changes must demonstrate same-session recovery and terminal completion there.
|
|
318
|
+
|
|
319
|
+
`npm run test:full` is reserved for explicit release validation; ordinary
|
|
320
|
+
changes run targeted tests and `npm test`. The control plane coordinating a run
|
|
321
|
+
is not replaced while that run is in flight.
|
|
322
|
+
|
|
302
323
|
## A visual walkthrough
|
|
303
324
|
|
|
304
325
|
### Control complexity
|
|
@@ -199,7 +199,7 @@ export interface ContinuationHooks {
|
|
|
199
199
|
toolStarted(sessionID: string, tool: string): void;
|
|
200
200
|
blocksTool(sessionID: string): boolean;
|
|
201
201
|
sessionIdle(sessionID: string): Promise<void>;
|
|
202
|
-
stopAutomaticRecovery(sessionID: string, abortSession?: boolean): Promise<void>;
|
|
202
|
+
stopAutomaticRecovery(sessionID: string, abortSession?: boolean, resumeOnRealUserTurn?: boolean): Promise<void>;
|
|
203
203
|
recoverStalledTask(sessionID: string, callIDs: readonly string[]): Promise<"recovered" | "identity-rejected" | "capability-unavailable" | "request-rejected">;
|
|
204
204
|
forgetSession(sessionID: string): void;
|
|
205
205
|
}
|
|
@@ -225,6 +225,7 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
225
225
|
const sessions = new Map();
|
|
226
226
|
const warned = new Set();
|
|
227
227
|
const stoppedSessions = new Set();
|
|
228
|
+
const realTurnResumableStops = new Set();
|
|
228
229
|
function observeTransition(type, sessionID, state, reason) {
|
|
229
230
|
try {
|
|
230
231
|
transitionObserver?.({
|
|
@@ -811,8 +812,9 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
811
812
|
}
|
|
812
813
|
sessions.delete(sessionID);
|
|
813
814
|
stoppedSessions.delete(sessionID);
|
|
815
|
+
realTurnResumableStops.delete(sessionID);
|
|
814
816
|
}
|
|
815
|
-
async function stopAutomaticRecovery(sessionID, abortSession = true) {
|
|
817
|
+
async function stopAutomaticRecovery(sessionID, abortSession = true, resumeOnRealUserTurn = false) {
|
|
816
818
|
const state = sessions.get(sessionID);
|
|
817
819
|
if (state !== undefined) {
|
|
818
820
|
clearTimer(state.cooldownTimer);
|
|
@@ -830,8 +832,14 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
830
832
|
}
|
|
831
833
|
stoppedSessions.delete(sessionID);
|
|
832
834
|
stoppedSessions.add(sessionID);
|
|
835
|
+
if (resumeOnRealUserTurn)
|
|
836
|
+
realTurnResumableStops.add(sessionID);
|
|
837
|
+
else
|
|
838
|
+
realTurnResumableStops.delete(sessionID);
|
|
833
839
|
while (stoppedSessions.size > MAX_TRACKED_SESSIONS) {
|
|
834
|
-
stoppedSessions.
|
|
840
|
+
const oldest = stoppedSessions.values().next().value;
|
|
841
|
+
stoppedSessions.delete(oldest);
|
|
842
|
+
realTurnResumableStops.delete(oldest);
|
|
835
843
|
}
|
|
836
844
|
const abort = abortSession ? client?.session?.abort : undefined;
|
|
837
845
|
if (abort !== undefined) {
|
|
@@ -842,6 +850,8 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
842
850
|
}
|
|
843
851
|
}
|
|
844
852
|
async function recoverStalledTask(sessionID, callIDs) {
|
|
853
|
+
if (stoppedSessions.has(sessionID))
|
|
854
|
+
return "request-rejected";
|
|
845
855
|
const active = policy();
|
|
846
856
|
const resolution = resolveContinuation({
|
|
847
857
|
identity: await readIdentity(sessionID),
|
|
@@ -863,6 +873,8 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
863
873
|
// Reserve durable authority before mutating the host session. A configured authority that
|
|
864
874
|
// cannot issue a ticket must not cause an abort/retry loop or an unproven synthetic send.
|
|
865
875
|
const metadata = await ticketMetadata(sessionID, `watchdog:${callIDs.join(",")}`);
|
|
876
|
+
if (stoppedSessions.has(sessionID))
|
|
877
|
+
return "request-rejected";
|
|
866
878
|
await abort.call(client.session, {
|
|
867
879
|
path: { id: sessionID },
|
|
868
880
|
query: { directory },
|
|
@@ -1164,8 +1176,11 @@ export function createContinuationHooks(client, directory, policySource, timings
|
|
|
1164
1176
|
output.enabled = false;
|
|
1165
1177
|
},
|
|
1166
1178
|
observeModel(sessionID, model, synthetic = false) {
|
|
1167
|
-
if (stoppedSessions.has(sessionID))
|
|
1168
|
-
|
|
1179
|
+
if (stoppedSessions.has(sessionID)) {
|
|
1180
|
+
if (synthetic || !realTurnResumableStops.delete(sessionID))
|
|
1181
|
+
return;
|
|
1182
|
+
stoppedSessions.delete(sessionID);
|
|
1183
|
+
}
|
|
1169
1184
|
if (!nonEmpty(model.providerID) || !nonEmpty(model.modelID))
|
|
1170
1185
|
return;
|
|
1171
1186
|
const state = stateFor(sessionID);
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -91,6 +91,11 @@ export type HandoffDenialReason = "configuration-unavailable" | "path-invalid" |
|
|
|
91
91
|
export type FreshSessionReason = "child-lineage" | "asset-contract-skew";
|
|
92
92
|
export type FreshSessionAction = "open-fresh-root" | "install-assets-then-open-fresh-root" | "restart-host-after-install";
|
|
93
93
|
export type FreshSessionResult = Readonly<{
|
|
94
|
+
status: "redispatch-queued";
|
|
95
|
+
reason: FreshSessionReason;
|
|
96
|
+
source_session_id: string;
|
|
97
|
+
retry_same_session: false;
|
|
98
|
+
}> | Readonly<{
|
|
94
99
|
status: "redispatched";
|
|
95
100
|
reason: FreshSessionReason;
|
|
96
101
|
source_session_id: string;
|
package/dist/plugin/index.js
CHANGED
|
@@ -483,15 +483,58 @@ function sameRelativePath(left, right) {
|
|
|
483
483
|
function textPart(part) {
|
|
484
484
|
return isRecord(part) && typeof part.text === "string" ? part.text : undefined;
|
|
485
485
|
}
|
|
486
|
+
const FILE_PART_KEYS = new Set(["id", "sessionID", "messageID", "type", "mime", "filename", "url", "source"]);
|
|
487
|
+
function safeFilePart(part) {
|
|
488
|
+
if (Object.keys(part).some((key) => !FILE_PART_KEYS.has(key)) ||
|
|
489
|
+
typeof part.mime !== "string" || part.mime.length === 0 ||
|
|
490
|
+
typeof part.url !== "string" || part.url.length === 0 ||
|
|
491
|
+
(part.filename !== undefined && typeof part.filename !== "string") ||
|
|
492
|
+
(part.id !== undefined && typeof part.id !== "string") ||
|
|
493
|
+
(part.sessionID !== undefined && typeof part.sessionID !== "string") ||
|
|
494
|
+
(part.messageID !== undefined && typeof part.messageID !== "string") ||
|
|
495
|
+
(part.source !== undefined && !isRecord(part.source)))
|
|
496
|
+
return undefined;
|
|
497
|
+
return {
|
|
498
|
+
type: "file",
|
|
499
|
+
mime: part.mime,
|
|
500
|
+
...(part.filename === undefined ? {} : { filename: part.filename }),
|
|
501
|
+
url: part.url,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
/** OpenCode expands text attachments into synthetic explanatory text alongside the real file.
|
|
505
|
+
* Those derived text parts carry no user authority and are never copied into a fresh prompt.
|
|
506
|
+
* A ticket-bearing synthetic turn (or a turn without real user text and a safe file) stays synthetic.
|
|
507
|
+
*/
|
|
508
|
+
function realAttachmentParts(parts) {
|
|
509
|
+
const hasUserText = parts.some((part) => isRecord(part) && part.type === "text" &&
|
|
510
|
+
part.synthetic !== true && typeof part.text === "string" && part.text.trim().length > 0);
|
|
511
|
+
const hasFile = parts.some((part) => isRecord(part) && part.type === "file" && part.synthetic !== true && safeFilePart(part) !== undefined);
|
|
512
|
+
if (!hasUserText || !hasFile || parts.some((part) => isRecord(part) && part.synthetic === true &&
|
|
513
|
+
(part.type !== "text" || typeof part.text !== "string" || part.metadata !== undefined)))
|
|
514
|
+
return parts;
|
|
515
|
+
return parts.filter((part) => !isRecord(part) || part.synthetic !== true);
|
|
516
|
+
}
|
|
517
|
+
function syntheticPrompt(parts) {
|
|
518
|
+
return realAttachmentParts(parts).some((part) => isRecord(part) && part.synthetic === true);
|
|
519
|
+
}
|
|
486
520
|
function freshSessionPrompt(parts) {
|
|
487
521
|
const prompt = [];
|
|
488
|
-
for (const part of parts) {
|
|
489
|
-
if (!isRecord(part) || part.
|
|
522
|
+
for (const part of realAttachmentParts(parts)) {
|
|
523
|
+
if (!isRecord(part) || part.synthetic === true)
|
|
490
524
|
return undefined;
|
|
525
|
+
if (part.type === "text" && typeof part.text === "string")
|
|
526
|
+
prompt.push({ type: "text", text: part.text });
|
|
527
|
+
else if (part.type === "file") {
|
|
528
|
+
const attachment = safeFilePart(part);
|
|
529
|
+
if (attachment === undefined)
|
|
530
|
+
return undefined;
|
|
531
|
+
prompt.push(attachment);
|
|
491
532
|
}
|
|
492
|
-
|
|
533
|
+
else
|
|
534
|
+
return undefined;
|
|
493
535
|
}
|
|
494
|
-
return prompt.length > 0 && prompt.some((
|
|
536
|
+
return prompt.length > 0 && prompt.some((part) => part.type === "text" && part.text.trim().length > 0)
|
|
537
|
+
? prompt : undefined;
|
|
495
538
|
}
|
|
496
539
|
/**
|
|
497
540
|
* One handoff entry per line. The coordinator asset emits inline digests as `key: value`, often
|
|
@@ -1282,10 +1325,6 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1282
1325
|
const messages = input.client?.session?.messages;
|
|
1283
1326
|
if (messages === undefined)
|
|
1284
1327
|
return undefined;
|
|
1285
|
-
const currentText = currentParts.filter(isRecord).filter((part) => part.type === "text" && part.synthetic !== true)
|
|
1286
|
-
.map((part) => part.text).filter((text) => typeof text === "string");
|
|
1287
|
-
if (currentText.length === 0)
|
|
1288
|
-
return undefined;
|
|
1289
1328
|
for (const delay of [0, 10, 50]) {
|
|
1290
1329
|
if (delay > 0)
|
|
1291
1330
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
@@ -1303,11 +1342,10 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1303
1342
|
continue;
|
|
1304
1343
|
const info = isRecord(message.info) ? message.info : undefined;
|
|
1305
1344
|
if ((info?.role ?? message.role) !== "user" || (info?.agent ?? message.agent) !== selectedAgent ||
|
|
1306
|
-
!Array.isArray(message.parts) || message.parts
|
|
1345
|
+
!Array.isArray(message.parts) || syntheticPrompt(message.parts))
|
|
1307
1346
|
continue;
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
if (JSON.stringify(persistedText) !== JSON.stringify(currentText))
|
|
1347
|
+
const persistedParts = freshSessionPrompt(message.parts);
|
|
1348
|
+
if (persistedParts === undefined || JSON.stringify(persistedParts) !== JSON.stringify(currentParts))
|
|
1311
1349
|
continue;
|
|
1312
1350
|
const messageID = info?.id ?? message.id;
|
|
1313
1351
|
if (typeof messageID === "string" && messageID.length > 0)
|
|
@@ -1503,16 +1541,24 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1503
1541
|
}
|
|
1504
1542
|
}
|
|
1505
1543
|
async function acceptRealGoalTurn(sessionID, messageID, selectedAgent, parts) {
|
|
1544
|
+
// Root recovery can prove the persisted user message identity before its parts are available.
|
|
1545
|
+
// Preserve that existing empty projection while every observed non-empty shape stays strict.
|
|
1546
|
+
const safeParts = parts.length === 0
|
|
1547
|
+
? []
|
|
1548
|
+
: freshSessionPrompt(parts);
|
|
1549
|
+
if (safeParts === undefined)
|
|
1550
|
+
throw new Error("SORTIE_GOAL_CONTROL_DENIED: unsafe-message-parts");
|
|
1506
1551
|
await recoverCompletedGoalReservations(sessionID);
|
|
1507
1552
|
const ledger = await goalLedger(sessionID);
|
|
1508
1553
|
const state = (await ledger.readGoal()).state;
|
|
1509
1554
|
if (state.latest_user_message_id === messageID)
|
|
1510
1555
|
return;
|
|
1511
1556
|
const at = new Date().toISOString();
|
|
1512
|
-
const explicitContinuation =
|
|
1557
|
+
const explicitContinuation = safeParts.filter((part) => part.type === "text")
|
|
1558
|
+
.map((part) => part.text).join("\n")
|
|
1513
1559
|
.split(/\r?\n/u).some((line) => /^\s*(?:goal_acceptance_fingerprint|goal_budget_(?:units|time_ms|cost_usd))\s*:/iu.test(line));
|
|
1514
1560
|
if (state.goal_id === null || state.phase === "terminal" || (state.phase === "stopped" && !explicitContinuation)) {
|
|
1515
|
-
const acceptance = goalFingerprint({ message_id: messageID, parts:
|
|
1561
|
+
const acceptance = goalFingerprint({ message_id: messageID, parts: safeParts.map((part) => part.type === "text" ? part.text : part) });
|
|
1516
1562
|
await ledger.appendGoal({ kind: "goal.accepted", at,
|
|
1517
1563
|
goal_id: goalFingerprint({ root: goalRoot(sessionID), origin_user_message_id: messageID }),
|
|
1518
1564
|
revision: 1, scope_epoch: 1, acceptance_fingerprint: acceptance,
|
|
@@ -1544,7 +1590,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1544
1590
|
const persistedInfo = isRecord(message.info) ? message.info : undefined;
|
|
1545
1591
|
if ((persistedInfo?.role ?? message.role) !== "user" ||
|
|
1546
1592
|
(persistedInfo?.agent ?? message.agent) !== COORDINATOR_AGENT || !Array.isArray(message.parts) ||
|
|
1547
|
-
message.parts
|
|
1593
|
+
syntheticPrompt(message.parts))
|
|
1548
1594
|
return;
|
|
1549
1595
|
await acceptRealGoalTurn(sessionID, info.id, COORDINATOR_AGENT, message.parts);
|
|
1550
1596
|
goalDeclarationAuthority.set(sessionID, info.id);
|
|
@@ -1685,9 +1731,6 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
1685
1731
|
else if (receipt === undefined && proved) {
|
|
1686
1732
|
receipt = await terminalGoal(sessionID, "completed", "succeeded").catch(() => undefined);
|
|
1687
1733
|
}
|
|
1688
|
-
else if (receipt === undefined && outcome === "DONE") {
|
|
1689
|
-
receipt = await terminalGoal(sessionID, "completed", "succeeded").catch(() => undefined);
|
|
1690
|
-
}
|
|
1691
1734
|
else if (receipt === undefined && outcome === "INTERRUPTED") {
|
|
1692
1735
|
receipt = await terminalGoal(sessionID, "stopped", "stopped").catch(() => undefined);
|
|
1693
1736
|
}
|
|
@@ -2184,57 +2227,67 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2184
2227
|
}
|
|
2185
2228
|
const key = `${sourceSessionID}\u0000${reason}`;
|
|
2186
2229
|
const existing = freshSessionRedispatches.get(key);
|
|
2230
|
+
const queued = { status: "redispatch-queued", reason,
|
|
2231
|
+
source_session_id: sourceSessionID, retry_same_session: false };
|
|
2187
2232
|
if (existing !== undefined)
|
|
2188
|
-
return await existing.operation;
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2233
|
+
return existing.settled ? await existing.operation : queued;
|
|
2234
|
+
// Both session.create and promptAsync enter the host request scheduler. Defer the entire
|
|
2235
|
+
// redispatch until the child chat hook has returned its typed control error.
|
|
2236
|
+
const operation = new Promise((accept) => {
|
|
2237
|
+
setTimeout(() => {
|
|
2238
|
+
void (async () => {
|
|
2239
|
+
let targetSessionID;
|
|
2240
|
+
try {
|
|
2241
|
+
const created = await create.call(input.client.session, {
|
|
2242
|
+
query: { directory: input.worktree ?? input.directory },
|
|
2243
|
+
body: {},
|
|
2244
|
+
});
|
|
2245
|
+
const payload = isRecord(created) && "data" in created ? created.data : created;
|
|
2246
|
+
if (!isRecord(payload) || typeof payload.id !== "string" || payload.id.length === 0) {
|
|
2247
|
+
return freshSessionFallback(reason, fallbackAction);
|
|
2248
|
+
}
|
|
2249
|
+
targetSessionID = payload.id;
|
|
2250
|
+
const parentID = typeof payload.parentID === "string" ? payload.parentID
|
|
2251
|
+
: typeof payload.parentId === "string" ? payload.parentId
|
|
2252
|
+
: undefined;
|
|
2253
|
+
if (parentID !== undefined) {
|
|
2254
|
+
await deleteFreshSession(targetSessionID);
|
|
2255
|
+
return freshSessionFallback(reason, fallbackAction);
|
|
2256
|
+
}
|
|
2257
|
+
goalRootSessions.set(targetSessionID, goalRoot(sourceSessionID));
|
|
2258
|
+
const ticket = await issueGoalTicket(targetSessionID, `fresh-root:${reason}`);
|
|
2259
|
+
if (!ticket.issued)
|
|
2260
|
+
throw new Error("fresh coordinator ticket already outstanding");
|
|
2261
|
+
const sendFresh = send;
|
|
2262
|
+
const sent = await sendFresh.call(input.client.session, {
|
|
2263
|
+
path: { id: targetSessionID },
|
|
2264
|
+
query: { directory: input.worktree ?? input.directory },
|
|
2265
|
+
body: { agent: COORDINATOR_AGENT, parts: prompt.map((part) => part.type === "text"
|
|
2266
|
+
? { ...part, synthetic: true, metadata: ticket.metadata }
|
|
2267
|
+
: part) },
|
|
2268
|
+
});
|
|
2269
|
+
if (!promptAccepted(sent))
|
|
2270
|
+
throw new Error("fresh coordinator prompt rejected");
|
|
2271
|
+
appLogInfo("fresh-session.redispatched", sourceSessionID, {
|
|
2272
|
+
reason,
|
|
2273
|
+
targetSessionID: targetSessionID.slice(0, 128),
|
|
2274
|
+
});
|
|
2275
|
+
return {
|
|
2276
|
+
status: "redispatched",
|
|
2277
|
+
reason,
|
|
2278
|
+
source_session_id: sourceSessionID,
|
|
2279
|
+
target_session_id: targetSessionID,
|
|
2280
|
+
retry_same_session: false,
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
catch {
|
|
2284
|
+
if (targetSessionID !== undefined)
|
|
2285
|
+
await deleteFreshSession(targetSessionID);
|
|
2286
|
+
return freshSessionFallback(reason, fallbackAction);
|
|
2287
|
+
}
|
|
2288
|
+
})().then(accept);
|
|
2289
|
+
}, 0);
|
|
2290
|
+
});
|
|
2238
2291
|
const entry = { operation, settled: false };
|
|
2239
2292
|
freshSessionRedispatches.set(key, entry);
|
|
2240
2293
|
void operation.then((result) => {
|
|
@@ -2257,7 +2310,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
2257
2310
|
break;
|
|
2258
2311
|
freshSessionRedispatches.delete(completed[0]);
|
|
2259
2312
|
}
|
|
2260
|
-
return
|
|
2313
|
+
return queued;
|
|
2261
2314
|
}
|
|
2262
2315
|
async function ensureLoaded() {
|
|
2263
2316
|
if (loaded?.gate !== undefined)
|
|
@@ -3428,9 +3481,6 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
3428
3481
|
contract_fingerprint: structuralAdmission.contract_fingerprint, experience: experience.trace });
|
|
3429
3482
|
}
|
|
3430
3483
|
const coordinator = await getParallelCoordinator();
|
|
3431
|
-
if (await coordinator.targetCheckedOut(`refs/heads/${structuralAdmission.contract.provenance.target_branch}`)) {
|
|
3432
|
-
return JSON.stringify({ status: "sol-serial", reason: "target-checked-out", experience: experience.trace });
|
|
3433
|
-
}
|
|
3434
3484
|
const result = await coordinator.prepareFabric(contract, ownerRoot, executionPlanPath === undefined ? undefined : await readJson(resolve(executionPlanPath), INPUT_LIMITS.parallel));
|
|
3435
3485
|
if (result.status === "sol-serial")
|
|
3436
3486
|
return JSON.stringify({ ...result, experience: experience.trace });
|
|
@@ -5020,15 +5070,19 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5020
5070
|
const agent = info?.agent ?? message.agent;
|
|
5021
5071
|
if (typeof agent !== "string")
|
|
5022
5072
|
return undefined;
|
|
5023
|
-
|
|
5073
|
+
const parts = message.parts === undefined ? [] : message.parts;
|
|
5074
|
+
if (!Array.isArray(parts))
|
|
5024
5075
|
return undefined;
|
|
5025
|
-
const synthetic =
|
|
5076
|
+
const synthetic = syntheticPrompt(parts);
|
|
5026
5077
|
if (synthetic)
|
|
5027
5078
|
continue;
|
|
5079
|
+
const persistedParts = parts.length === 0 ? [] : freshSessionPrompt(parts);
|
|
5080
|
+
if (persistedParts === undefined)
|
|
5081
|
+
return undefined;
|
|
5028
5082
|
const messageID = info?.id ?? message.id;
|
|
5029
5083
|
if (typeof messageID !== "string" || messageID.length === 0)
|
|
5030
5084
|
return undefined;
|
|
5031
|
-
persistedTurn = { agent, synthetic: false, messageID };
|
|
5085
|
+
persistedTurn = { agent, synthetic: false, messageID, parts: persistedParts };
|
|
5032
5086
|
break;
|
|
5033
5087
|
}
|
|
5034
5088
|
return { hasForeignUserTurn: persistedTurn?.agent !== undefined && persistedTurn.agent !== COORDINATOR_AGENT,
|
|
@@ -5124,7 +5178,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5124
5178
|
return false;
|
|
5125
5179
|
await rememberCoordinatorRoot(sessionID);
|
|
5126
5180
|
if (persistedTurn !== undefined) {
|
|
5127
|
-
await acceptRealGoalTurn(sessionID, persistedTurn.messageID, persistedTurn.agent,
|
|
5181
|
+
await acceptRealGoalTurn(sessionID, persistedTurn.messageID, persistedTurn.agent, persistedTurn.parts);
|
|
5128
5182
|
goalDeclarationAuthority.set(sessionID, persistedTurn.messageID);
|
|
5129
5183
|
}
|
|
5130
5184
|
await pinAssetVersion(sessionID);
|
|
@@ -5563,16 +5617,23 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5563
5617
|
.replaceAll(CONTINUATION_MARKER, "")
|
|
5564
5618
|
.trimEnd();
|
|
5565
5619
|
}
|
|
5620
|
+
const hostRunOutcome = terminalRunOutcome(textOutput.text);
|
|
5566
5621
|
textOutput.text = await preserveActiveGoalContinuation(textInput.sessionID, textOutput.text, textInput.messageID);
|
|
5567
|
-
|
|
5622
|
+
// Preserve a host DONE claim for proof checks while allowing an ordinary local INTERRUPTED
|
|
5623
|
+
// response to remain presentation-only IN_PROGRESS continuation.
|
|
5624
|
+
const runOutcome = hostRunOutcome === "DONE" ? hostRunOutcome : terminalRunOutcome(textOutput.text);
|
|
5568
5625
|
const terminal = runOutcome === undefined || !isCoordinatorSession(textInput.sessionID)
|
|
5569
5626
|
? undefined
|
|
5570
5627
|
: await terminalGoalFromHostText(textInput.sessionID, textOutput.text);
|
|
5571
|
-
if (runOutcome === "DONE" && terminal
|
|
5572
|
-
(
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5628
|
+
if (runOutcome === "DONE" && terminal?.delivery === "running") {
|
|
5629
|
+
textOutput.text = replaceDoneTerminalStatus(textOutput.text, "status: IN_PROGRESS — durable delivery active; same sessionでjoinまたはstale reconcileが必要");
|
|
5630
|
+
await continuation.stopAutomaticRecovery(textInput.sessionID, false, true);
|
|
5631
|
+
}
|
|
5632
|
+
else if (runOutcome === "DONE" && terminal?.receipt === undefined &&
|
|
5633
|
+
terminal?.goal !== undefined && terminal.goal.goal_id !== null) {
|
|
5634
|
+
textOutput.text = replaceDoneTerminalStatus(textOutput.text, "status: INTERRUPTED — accepted criteria remain unproved\n" +
|
|
5635
|
+
"TRUE_INTERRUPTION: internal: accepted criteria remain unproved");
|
|
5636
|
+
await continuation.stopAutomaticRecovery(textInput.sessionID, false, true);
|
|
5576
5637
|
}
|
|
5577
5638
|
if (runOutcome !== "DONE" && terminal?.delivery === "ready" && terminal.receipt?.status === "succeeded") {
|
|
5578
5639
|
textOutput.text = replaceTerminalStatus(textOutput.text, "status: DONE");
|
|
@@ -5666,7 +5727,7 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5666
5727
|
observedChildTerminals.delete(chatInput.sessionID);
|
|
5667
5728
|
await serializeChatTransition(chatInput.sessionID, async () => {
|
|
5668
5729
|
const parentID = chatParentID(chatInput);
|
|
5669
|
-
const synthetic = output.parts
|
|
5730
|
+
const synthetic = syntheticPrompt(output.parts);
|
|
5670
5731
|
const selectedAgent = chatInput.agent ?? output.message.agent;
|
|
5671
5732
|
if (parentID !== undefined)
|
|
5672
5733
|
rememberParent(chatInput.sessionID, parentID);
|
|
@@ -5676,8 +5737,15 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5676
5737
|
output.message.agent = chatInput.agent;
|
|
5677
5738
|
}
|
|
5678
5739
|
const requestedCoordinator = selectedAgent === COORDINATOR_AGENT;
|
|
5740
|
+
const projectedCoordinatorParts = requestedCoordinator && !synthetic && output.parts.length > 0
|
|
5741
|
+
? freshSessionPrompt(output.parts)
|
|
5742
|
+
: undefined;
|
|
5743
|
+
if (requestedCoordinator && !synthetic && output.parts.length > 0 && projectedCoordinatorParts === undefined) {
|
|
5744
|
+
throw new Error("SORTIE_GOAL_CONTROL_DENIED: unsafe-message-parts");
|
|
5745
|
+
}
|
|
5679
5746
|
const messageID = realMessageID(chatInput, output) ?? (synthetic ? undefined
|
|
5680
|
-
:
|
|
5747
|
+
: projectedCoordinatorParts === undefined ? undefined
|
|
5748
|
+
: await persistedCurrentRealMessageID(chatInput.sessionID, selectedAgent, projectedCoordinatorParts));
|
|
5681
5749
|
if (requestedCoordinator && !coordinatorRoot && (parentID !== undefined || knownChildSessions.has(chatInput.sessionID)) &&
|
|
5682
5750
|
!synthetic && messageID !== undefined) {
|
|
5683
5751
|
await acceptRealGoalTurn(chatInput.sessionID, messageID, selectedAgent, output.parts);
|
|
@@ -5724,11 +5792,11 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
5724
5792
|
// Some native hosts persist the user message only after this hook returns. Defer to the
|
|
5725
5793
|
// system-transform boundary, but retain no synthetic authority and accept only the exact
|
|
5726
5794
|
// final persisted real-user parts through persistedCurrentRealMessageID.
|
|
5727
|
-
pendingRealGoalTurns.set(chatInput.sessionID, { selectedAgent, parts: [
|
|
5795
|
+
pendingRealGoalTurns.set(chatInput.sessionID, { selectedAgent, parts: projectedCoordinatorParts ?? [] });
|
|
5728
5796
|
pruneParallelChildMap(pendingRealGoalTurns);
|
|
5729
5797
|
schedulePendingRealGoalRecovery(chatInput.sessionID);
|
|
5730
5798
|
}
|
|
5731
|
-
const prompt =
|
|
5799
|
+
const prompt = projectedCoordinatorParts;
|
|
5732
5800
|
if (prompt !== undefined)
|
|
5733
5801
|
coordinatorPrompts.set(chatInput.sessionID, prompt);
|
|
5734
5802
|
// Synthetic coordinator turns reach here only after consumeGoalTicket accepted the
|
|
@@ -6679,14 +6747,19 @@ export const SortieDogsPlugin = async (input, options) => {
|
|
|
6679
6747
|
return;
|
|
6680
6748
|
// Deletion is terminal for watchdog recovery. Disarm synchronously before any generic event
|
|
6681
6749
|
// processing can await, touch activity, or let a queued sweep recover the cancelled root.
|
|
6682
|
-
if (event.type === "session.deleted")
|
|
6750
|
+
if (event.type === "session.deleted") {
|
|
6683
6751
|
disarmDeletedCoordinatorTaskWatchdog(eventSessionID);
|
|
6752
|
+
await continuation.stopAutomaticRecovery(eventSessionID, false);
|
|
6753
|
+
}
|
|
6684
6754
|
if (event.type === "message.updated" && info !== undefined) {
|
|
6685
6755
|
rememberCoordinatorInterruption(eventSessionID, info);
|
|
6686
|
-
|
|
6756
|
+
if (pendingRealGoalTurns.has(eventSessionID))
|
|
6757
|
+
await recoverPendingRealGoalTurn(eventSessionID);
|
|
6758
|
+
else
|
|
6759
|
+
await acceptPersistedRealGoalEvent(eventSessionID, info);
|
|
6687
6760
|
}
|
|
6688
6761
|
if (pendingRealGoalTurns.has(eventSessionID) &&
|
|
6689
|
-
|
|
6762
|
+
event.type === "message.part.updated") {
|
|
6690
6763
|
await recoverPendingRealGoalTurn(eventSessionID);
|
|
6691
6764
|
}
|
|
6692
6765
|
const eventPartTime = isRecord(eventPart?.time) ? eventPart.time : undefined;
|
|
@@ -112,7 +112,15 @@ type SortieGoalSnapshot = Pick<GoalFlightState, "acceptance_contract" | "consume
|
|
|
112
112
|
export declare function createSortieResult(receipt: GoalTerminalReceipt, goal: SortieGoalSnapshot, metrics: RunMetrics | undefined, asOf?: string, records?: readonly GoalFlightEventRecord[]): SortieResult;
|
|
113
113
|
export type RunTerminalOutcome = "DONE" | "INTERRUPTED" | "BLOCKED" | "NEED_DECISION";
|
|
114
114
|
export declare function collectRunMetrics(client: RunMetricsClient | undefined, rootSessionID: string, directory?: string, now?: number, window?: RunMetricsWindow): Promise<RunMetrics | undefined>;
|
|
115
|
-
export
|
|
115
|
+
export interface SortieResultPresentation {
|
|
116
|
+
readonly implementation?: string;
|
|
117
|
+
readonly pending?: string;
|
|
118
|
+
readonly next?: string;
|
|
119
|
+
readonly commit?: string;
|
|
120
|
+
readonly statusSummary?: string;
|
|
121
|
+
readonly stopReason?: string;
|
|
122
|
+
}
|
|
123
|
+
export declare function formatSortieResult(result: SortieResult, presentation?: SortieResultPresentation): string;
|
|
116
124
|
export declare function formatRunMetrics(metrics: RunMetrics): string;
|
|
117
125
|
export declare function isDoneTerminalText(text: string): boolean;
|
|
118
126
|
export declare function terminalRunOutcome(text: string): RunTerminalOutcome | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildDebrief, renderDebrief, observeDebriefSession } from "./sortie-debrief.js";
|
|
1
|
+
import { buildDebrief, renderDebrief, renderDebriefProof, observeDebriefSession } from "./sortie-debrief.js";
|
|
2
2
|
import { goalFingerprint } from "../core/goal-bound.js";
|
|
3
3
|
import { renderCareer } from "./sortie-career.js";
|
|
4
4
|
const unavailable = (reason) => ({ availability: "unavailable", value: null, reason });
|
|
@@ -341,25 +341,61 @@ function duration(milliseconds) {
|
|
|
341
341
|
function metricText(metric, render) {
|
|
342
342
|
return metric.availability === "available" ? render(metric.value) : "計測不可";
|
|
343
343
|
}
|
|
344
|
-
|
|
344
|
+
const displayText = (value) => value?.replace(/[\r\n\t]+/gu, " ").trim() || "未取得";
|
|
345
|
+
const reportFence = (body) => {
|
|
346
|
+
const longest = Math.max(0, ...[...body.matchAll(/^~+/gmu)].map((match) => match[0].length));
|
|
347
|
+
const fence = "~".repeat(Math.max(3, longest + 1));
|
|
348
|
+
return `${fence}text\n${body}\n${fence}`;
|
|
349
|
+
};
|
|
350
|
+
export function formatSortieResult(result, presentation = {}) {
|
|
345
351
|
const criteria = metricText(result.proof.criteria, (entries) => {
|
|
346
352
|
const passing = entries.filter(({ status }) => status === "PASS").length;
|
|
347
353
|
return `${passing}/${entries.length}`;
|
|
348
354
|
});
|
|
349
|
-
const achievement = result.mission.status === "COMPLETED" ? "
|
|
355
|
+
const achievement = result.mission.status === "COMPLETED" ? "COMPLETED"
|
|
356
|
+
: result.mission.status === "INTERRUPTED" ? "INTERRUPTED"
|
|
357
|
+
: result.mission.status === "EXTERNAL_BLOCKER" ? "EXTERNAL_BLOCKER" : "USER_DECISION";
|
|
358
|
+
const summaryLabel = result.mission.status === "COMPLETED" ? "完了"
|
|
350
359
|
: result.mission.status === "INTERRUPTED" ? "中断(未完了)"
|
|
351
|
-
: result.mission.status === "EXTERNAL_BLOCKER" ? "外部要因で未完了"
|
|
352
|
-
: "ユーザー判断待ち(未完了)";
|
|
360
|
+
: result.mission.status === "EXTERNAL_BLOCKER" ? "外部要因で未完了" : "ユーザー判断待ち(未完了)";
|
|
353
361
|
const color = result.mission.status === "COMPLETED" ? "🟢" : result.mission.status === "EXTERNAL_BLOCKER" ? "🔴" : "🟡";
|
|
362
|
+
const proof = renderDebriefProof(result.debrief);
|
|
354
363
|
const body = [
|
|
355
|
-
|
|
356
|
-
|
|
364
|
+
"🐾 SORTIE DOGS — 帰還報告",
|
|
365
|
+
result.result_id[0],
|
|
366
|
+
"",
|
|
367
|
+
`${color} ${achievement} — ${displayText(presentation.statusSummary)}`,
|
|
368
|
+
"",
|
|
369
|
+
"⚔️ MISSION",
|
|
370
|
+
`経過 ⏱ ${metricText(result.speed.goal_wall_ms, duration)} ※待機含む`,
|
|
371
|
+
`最終達成条件 ◔ ${criteria}`,
|
|
372
|
+
`対象検証 ${proof.validation}`,
|
|
373
|
+
`SourceReview ${proof.review}`,
|
|
374
|
+
`Commit ${displayText(presentation.commit)}`,
|
|
375
|
+
"",
|
|
376
|
+
"🔧 実装",
|
|
377
|
+
displayText(presentation.implementation),
|
|
378
|
+
"",
|
|
379
|
+
"⏳ 未実施",
|
|
380
|
+
displayText(presentation.pending),
|
|
381
|
+
"",
|
|
382
|
+
"➡️ NEXT",
|
|
383
|
+
displayText(presentation.next),
|
|
384
|
+
"",
|
|
385
|
+
"🪙 COST / PACK",
|
|
386
|
+
`使用量 ${metricText(result.cost.total_tokens, (value) => `${value.toLocaleString("ja-JP")} tokens`)}`,
|
|
387
|
+
`host推定額 ${metricText(result.cost.cost_usd, (value) => `$${value.toFixed(4)}`)} ※実課金換算なし`,
|
|
388
|
+
"",
|
|
357
389
|
...renderDebrief(result.debrief),
|
|
358
|
-
|
|
359
|
-
"*最終応答生成前の計測*",
|
|
390
|
+
"",
|
|
360
391
|
...renderCareer(result.career),
|
|
361
|
-
|
|
362
|
-
|
|
392
|
+
"",
|
|
393
|
+
"🛑 STOP REASON",
|
|
394
|
+
displayText(presentation.stopReason ?? result.mission.stop_reason),
|
|
395
|
+
"",
|
|
396
|
+
"※使用量は最終応答生成前の計測",
|
|
397
|
+
].join("\n");
|
|
398
|
+
return `<details>\n<summary><strong>🐾 SORTIE DOGS — 帰還報告|${color} ${summaryLabel}</strong></summary>\n\n${reportFence(body)}\n\n</details>`;
|
|
363
399
|
}
|
|
364
400
|
export function formatRunMetrics(metrics) {
|
|
365
401
|
const elapsed = metrics.durationMilliseconds === undefined ? "duration unavailable" : `${duration(metrics.durationMilliseconds)} wall-clock`;
|
|
@@ -476,6 +512,7 @@ export function insertRunMetrics(text, metrics) {
|
|
|
476
512
|
return lines.join(newline);
|
|
477
513
|
}
|
|
478
514
|
export function insertSortieResult(text, result) {
|
|
515
|
+
const presentation = extractSortiePresentation(text);
|
|
479
516
|
const visible = sanitizeTerminalReport(text);
|
|
480
517
|
const checkpoint = terminalCheckpoint(visible);
|
|
481
518
|
if (checkpoint === undefined)
|
|
@@ -506,7 +543,7 @@ export function insertSortieResult(text, result) {
|
|
|
506
543
|
cleaned.splice(checkpoint.index + 1, 1);
|
|
507
544
|
let card;
|
|
508
545
|
try {
|
|
509
|
-
card = formatSortieResult(result);
|
|
546
|
+
card = formatSortieResult(result, presentation);
|
|
510
547
|
}
|
|
511
548
|
catch {
|
|
512
549
|
card = "<details>\n<summary><strong>🐾 SORTIE DOGS — 帰還報告</strong></summary>\n\n**確認:** 表示集計を取得できません。任務結果は先頭の状態を参照。\n\n</details>";
|
|
@@ -514,6 +551,23 @@ export function insertSortieResult(text, result) {
|
|
|
514
551
|
cleaned.splice(checkpoint.index + 1, 0, "", card, "");
|
|
515
552
|
return cleaned.join(newline).trimEnd();
|
|
516
553
|
}
|
|
554
|
+
function extractSortiePresentation(text) {
|
|
555
|
+
const first = topLevelLines(text).find(({ line }) => line.trim().length > 0)?.line ?? "";
|
|
556
|
+
const statusSummary = first.split(/\s+[—-]\s+/u).slice(1).join(" — ").trim() || undefined;
|
|
557
|
+
const section = (names) => {
|
|
558
|
+
const expression = new RegExp(`^[ \\t]*(?:#{1,6}[ \\t]*)?(?:\\*\\*)?(?:${names})(?:\\*\\*)?[ \\t]*:?[ \\t]*(?:\\*\\*)?[ \\t]*(.*)$`, "imu");
|
|
559
|
+
const match = expression.exec(text);
|
|
560
|
+
if (match === null)
|
|
561
|
+
return undefined;
|
|
562
|
+
if (match[1]?.trim())
|
|
563
|
+
return match[1].trim();
|
|
564
|
+
const tail = text.slice(match.index + match[0].length).split(/\r?\n/u);
|
|
565
|
+
return tail.find((line) => line.trim().length > 0 && !/^[ \\t]*(?:#{1,6}[ \\t]*)?(?:\\*\\*)?(?:変更点|実装|未実施|次|NEXT|Commit|コミット)/iu.test(line))?.trim();
|
|
566
|
+
};
|
|
567
|
+
const explicitStop = /^(?:TRUE_INTERRUPTION|TRUE_BLOCKER)[ \\t]*:[ \\t]*(.+)$/imu.exec(text)?.[1]?.trim();
|
|
568
|
+
return { statusSummary, implementation: section("変更点|実装"), pending: section("未実施"), next: section("次|NEXT"),
|
|
569
|
+
commit: section("Commit|コミット"), stopReason: explicitStop };
|
|
570
|
+
}
|
|
517
571
|
export function createGoalReport(result, receipt) {
|
|
518
572
|
const tokens = result.cost.total_tokens.availability === "available" && Number.isSafeInteger(result.cost.total_tokens.value)
|
|
519
573
|
? result.cost.total_tokens.value : null;
|
|
@@ -124,21 +124,25 @@ export async function collectCareer(directories, currentPath, current, read, max
|
|
|
124
124
|
}
|
|
125
125
|
export function renderCareer(career) {
|
|
126
126
|
if (career === undefined)
|
|
127
|
-
return ["
|
|
127
|
+
return ["📜 PACK RECORD", "保存履歴を取得できません"];
|
|
128
128
|
const terminal = career.goals - career.active;
|
|
129
|
-
const
|
|
130
|
-
const minutes = (metric) => metric.covered === 0 ? "計測不可" : `${(metric.sum / 60000).toFixed(1)}分(${metric.covered}/${terminal}任務)`;
|
|
131
|
-
const models = [...career.models].sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model));
|
|
132
|
-
const modelText = models.slice(0, 4).map((entry) => `${entry.model.replace(/[\\`*_{}\[\]()<>!|\r\n]/gu, "").slice(0, 120)} ${entry.tokens.toLocaleString("ja-JP")}`).join(" · ");
|
|
129
|
+
const minutes = (metric) => metric.covered === 0 ? "計測不可" : `${(metric.sum / 60000).toFixed(1)}分 ※${metric.covered}/${terminal}任務`;
|
|
133
130
|
return [
|
|
134
|
-
"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
131
|
+
"📜 PACK RECORD",
|
|
132
|
+
`🏁 完了 ${career.completed}`,
|
|
133
|
+
`🟡 中断 ${career.interrupted}`,
|
|
134
|
+
`⏳ 外部待機 ${career.external}`,
|
|
135
|
+
`❓ 指示待ち ${career.decision}`,
|
|
136
|
+
`🔄 進行中 ${career.active}`,
|
|
137
|
+
`↩️ 復帰 ${career.recoveries}`,
|
|
138
|
+
"",
|
|
139
|
+
`🪙 累積使用量 ${career.tokens.covered === 0 ? "計測不可" : `${career.tokens.sum.toLocaleString("ja-JP")} tokens`} ※${career.tokens.covered}/${terminal}任務`,
|
|
140
|
+
`⏱ 累積worker ${minutes(career.workerTime)}`,
|
|
141
|
+
`🕰 累積goal ${minutes(career.goalWall)} ※待機・重複含む`,
|
|
142
|
+
`⚡ 累積重複率 ${career.overlap.ratio === null ? "計測不可" : `${career.overlap.ratio.toFixed(2)}×`} ※${career.overlap.covered}/${terminal}任務・速度倍率ではありません`,
|
|
143
|
+
"",
|
|
144
|
+
"📦 保存範囲",
|
|
145
|
+
`${career.since?.slice(0, 10) ?? "開始日不明"}以降 / ${career.coverage.included} of ${career.coverage.files} files${career.coverage.unavailable || career.coverage.truncated ? " ※部分集計" : ""}`,
|
|
146
|
+
"※生涯戦績ではありません",
|
|
143
147
|
];
|
|
144
148
|
}
|
|
@@ -55,4 +55,8 @@ export interface Debrief {
|
|
|
55
55
|
export declare function observeDebriefSession(id: string, root: boolean, messages: readonly Record<string, unknown>[], window?: Span): DebriefSession;
|
|
56
56
|
export declare function buildDebrief(receipt: GoalTerminalReceipt, contract: GoalAcceptanceContract | null, observation: DebriefObservation | undefined, records?: readonly GoalFlightEventRecord[]): Debrief;
|
|
57
57
|
export declare function renderDebrief(debrief: Debrief | undefined): string[];
|
|
58
|
+
export declare function renderDebriefProof(debrief: Debrief | undefined): {
|
|
59
|
+
validation: string;
|
|
60
|
+
review: string;
|
|
61
|
+
};
|
|
58
62
|
export {};
|
|
@@ -254,6 +254,12 @@ export function buildDebrief(receipt, contract, observation, records) {
|
|
|
254
254
|
wallMilliseconds: unionDuration(spans) } } : {}) };
|
|
255
255
|
}
|
|
256
256
|
const label = (text) => text.replace(/[\r\n\t]/gu, " ").replace(/[\\`*_{}\[\]()<>!|]/gu, "").slice(0, 120);
|
|
257
|
+
const gauge = (percent) => {
|
|
258
|
+
const eighths = Math.max(0, Math.min(80, Math.round(percent * 0.8)));
|
|
259
|
+
const whole = Math.floor(eighths / 8), remainder = eighths % 8;
|
|
260
|
+
const partial = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"][remainder];
|
|
261
|
+
return `${"█".repeat(whole)}${partial}${" ".repeat(10 - whole - (remainder === 0 ? 0 : 1))}`;
|
|
262
|
+
};
|
|
257
263
|
export function renderDebrief(debrief) {
|
|
258
264
|
const pack = debrief?.pack == null ? null : [...debrief.pack].sort((a, b) => b.count - a.count || a.model.localeCompare(b.model));
|
|
259
265
|
const packVisible = pack?.slice(0, 4) ?? [];
|
|
@@ -264,21 +270,24 @@ export function renderDebrief(debrief) {
|
|
|
264
270
|
if (mix !== null && mix.length > 4)
|
|
265
271
|
visible.push({ model: "その他", tokens: mix.slice(4).reduce((sum, entry) => sum + entry.tokens, 0),
|
|
266
272
|
percent: mix.slice(4).reduce((sum, entry) => sum + entry.percent, 0) });
|
|
267
|
-
const
|
|
268
|
-
const filled = Math.max(0, Math.min(10, Math.round(percent / 10)));
|
|
269
|
-
return "█".repeat(filled) + "░".repeat(10 - filled);
|
|
270
|
-
};
|
|
271
|
-
const status = (value) => value === "PASS" ? "🟢 **PASS**" : value === "FAIL" ? "🔴 **FAIL**"
|
|
273
|
+
const status = (value) => value === "PASS" ? "🟢 PASS" : value === "FAIL" ? "🔴 FAIL"
|
|
272
274
|
: value === "WAIVED" ? "免除" : "未記録";
|
|
275
|
+
const counts = new Map(packVisible.map((entry) => [entry.model, entry.count]));
|
|
273
276
|
return [
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
...(mix === null ? ["モデル内訳 usage未取得"] : visible.map((entry) => {
|
|
278
|
+
const count = counts.get(entry.model);
|
|
279
|
+
return `🐕 ${label(entry.model)} ${gauge(entry.percent)} ${entry.percent.toFixed(1)}% ${entry.tokens.toLocaleString("ja-JP")} tokens${count === undefined ? "" : ` ×${count}`}`;
|
|
280
|
+
})),
|
|
281
|
+
`⚡ 実行重複率 ${debrief?.overlap !== undefined && debrief.overlap.wallMilliseconds > 0
|
|
282
|
+
? `${(debrief.overlap.workerMilliseconds / debrief.overlap.wallMilliseconds).toFixed(2)}×`
|
|
279
283
|
: pack?.length === 0 ? "対象なし(出撃なし)" : "稼働区間の記録不足"}`,
|
|
280
|
-
|
|
281
|
-
...(debrief?.notes?.length ? [
|
|
282
|
-
...(debrief?.traits.length ? [`**🏅 今回の戦績:** ${debrief.traits.join(" · ")}`] : []),
|
|
284
|
+
" ※worker区間・速度倍率ではありません",
|
|
285
|
+
...(debrief?.notes?.length ? [`計測範囲 ${debrief.notes.join(" · ")}`] : []),
|
|
283
286
|
];
|
|
284
287
|
}
|
|
288
|
+
export function renderDebriefProof(debrief) {
|
|
289
|
+
const status = (value) => value === "PASS" ? "🟢 PASS" : value === "FAIL" ? "🔴 FAIL"
|
|
290
|
+
: value === "WAIVED" ? "免除" : "未記録";
|
|
291
|
+
return { validation: status(debrief?.validation ?? "未確認"),
|
|
292
|
+
review: `${status(debrief?.review ?? "未確認")}${debrief?.reviewSource === "reviewer" ? "(reviewer報告)" : ""}` };
|
|
293
|
+
}
|