taskchef 7.3.0 → 7.5.0
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/.codex-plugin/plugin.json +1 -1
- package/README.md +58 -15
- package/assets/taskchef-dispatcher-instructions.md +9 -0
- package/docs/images/result-history-dashboard.jpg +0 -0
- package/docs/spec.md +99 -17
- package/docs/workflows.md +75 -17
- package/index.js +10 -0
- package/mcp/server.js +12 -0
- package/package.json +2 -1
- package/skills/taskchef-bootstrap/SKILL.md +4 -2
- package/skills/taskchef-report/SKILL.md +8 -6
- package/src/cli.js +40 -6
- package/src/dashboard/app.js +63 -5
- package/src/dashboard/index.html +2 -2
- package/src/dashboard/styles.css +8 -0
- package/src/dashboard-manager.js +183 -0
- package/src/dashboard.js +57 -1
- package/src/mcp.js +56 -2
- package/src/version.js +7 -0
- package/src/workspace.js +164 -68
package/src/workspace.js
CHANGED
|
@@ -38,8 +38,9 @@ const DISPATCH_FILE_NAME = "tasks.jsonl";
|
|
|
38
38
|
const WORKSPACE_LOCK_NAME = ".taskchef-workspace.lock";
|
|
39
39
|
const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
40
40
|
const CURRENT_CONFIG_SCHEMA_VERSION = 2;
|
|
41
|
-
const CURRENT_TASK_SCHEMA_VERSION =
|
|
42
|
-
const
|
|
41
|
+
const CURRENT_TASK_SCHEMA_VERSION = 6;
|
|
42
|
+
const PREVIOUS_TASK_SCHEMA_VERSION = 5;
|
|
43
|
+
const FIRST_SELF_LINKING_TASK_SCHEMA_VERSION = 4;
|
|
43
44
|
const CONFIG_FIELDS = new Set(["schemaVersion", "projects"]);
|
|
44
45
|
const PROJECT_FIELDS = new Set([
|
|
45
46
|
"name",
|
|
@@ -63,7 +64,8 @@ const STATEFUL_DISPATCH_FIELDS = new Set([
|
|
|
63
64
|
"updatedAt",
|
|
64
65
|
"updatedBy",
|
|
65
66
|
]);
|
|
66
|
-
const
|
|
67
|
+
const SCHEMA_5_DISPATCH_FIELDS = new Set([...STATEFUL_DISPATCH_FIELDS, "lastResult"]);
|
|
68
|
+
const DISPATCH_FIELDS = new Set([...STATEFUL_DISPATCH_FIELDS, "results"]);
|
|
67
69
|
const RECORD_DISPATCH_FIELDS = new Set([
|
|
68
70
|
"id",
|
|
69
71
|
"project",
|
|
@@ -75,7 +77,7 @@ const RESULT_STATUSES = new Set(["needs_input", "completed", "failed"]);
|
|
|
75
77
|
const TASK_STATUSES = new Set(["working", ...RESULT_STATUSES]);
|
|
76
78
|
const TASK_UPDATE_SOURCES = new Set(["dispatcher", "mcp"]);
|
|
77
79
|
const MAX_RESULT_SUMMARY_LENGTH = 2_000;
|
|
78
|
-
const
|
|
80
|
+
const RESULT_FIELDS = new Set(["status", "summary", "turnId", "updatedAt"]);
|
|
79
81
|
|
|
80
82
|
function requireExactFields(value, fields, name) {
|
|
81
83
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -221,7 +223,9 @@ async function appendDispatchesAtomic(workspaceRoot, dispatches) {
|
|
|
221
223
|
if (dispatches.length === 0) return;
|
|
222
224
|
const dispatchPath = path.join(workspaceRoot, DISPATCH_FILE_NAME);
|
|
223
225
|
const content = await readFile(dispatchPath, "utf8");
|
|
224
|
-
const appended = dispatches
|
|
226
|
+
const appended = dispatches
|
|
227
|
+
.map((dispatch) => `${JSON.stringify(schema6Task(dispatch))}\n`)
|
|
228
|
+
.join("");
|
|
225
229
|
await writeTextAtomic(dispatchPath, `${content}${appended}`);
|
|
226
230
|
}
|
|
227
231
|
|
|
@@ -679,7 +683,8 @@ export async function removeProject(workspaceRoot, name) {
|
|
|
679
683
|
|
|
680
684
|
async function validateDispatchShape(dispatch, name = "task") {
|
|
681
685
|
const supportedVersions = [
|
|
682
|
-
|
|
686
|
+
FIRST_SELF_LINKING_TASK_SCHEMA_VERSION,
|
|
687
|
+
PREVIOUS_TASK_SCHEMA_VERSION,
|
|
683
688
|
CURRENT_TASK_SCHEMA_VERSION,
|
|
684
689
|
];
|
|
685
690
|
if (!supportedVersions.includes(dispatch?.schemaVersion)) {
|
|
@@ -687,9 +692,11 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
687
692
|
}
|
|
688
693
|
requireExactFields(
|
|
689
694
|
dispatch,
|
|
690
|
-
dispatch.schemaVersion
|
|
695
|
+
dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
|
|
691
696
|
? DISPATCH_FIELDS
|
|
692
|
-
:
|
|
697
|
+
: dispatch.schemaVersion === PREVIOUS_TASK_SCHEMA_VERSION
|
|
698
|
+
? SCHEMA_5_DISPATCH_FIELDS
|
|
699
|
+
: STATEFUL_DISPATCH_FIELDS,
|
|
693
700
|
name,
|
|
694
701
|
);
|
|
695
702
|
const id = requireSafeId(dispatch.id, `${name}.id`);
|
|
@@ -700,38 +707,48 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
700
707
|
});
|
|
701
708
|
const turnId = optionalString(dispatch.turnId, `${name}.turnId`, { maxLength: 256 });
|
|
702
709
|
const updatedAt = requireTimestamp(dispatch.updatedAt, `${name}.updatedAt`);
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
requireExactFields(dispatch.lastResult, LAST_RESULT_FIELDS, `${name}.lastResult`);
|
|
707
|
-
lastResult = {
|
|
710
|
+
const normalizeResult = (result, resultName) => {
|
|
711
|
+
requireExactFields(result, RESULT_FIELDS, resultName);
|
|
712
|
+
const normalizedResult = {
|
|
708
713
|
status: requireEnum(
|
|
709
|
-
|
|
714
|
+
result.status,
|
|
710
715
|
RESULT_STATUSES,
|
|
711
|
-
`${
|
|
716
|
+
`${resultName}.status`,
|
|
712
717
|
),
|
|
713
718
|
summary: optionalString(
|
|
714
|
-
|
|
715
|
-
`${
|
|
719
|
+
result.summary,
|
|
720
|
+
`${resultName}.summary`,
|
|
716
721
|
{ maxLength: MAX_RESULT_SUMMARY_LENGTH },
|
|
717
722
|
),
|
|
718
723
|
turnId: optionalString(
|
|
719
|
-
|
|
720
|
-
`${
|
|
724
|
+
result.turnId,
|
|
725
|
+
`${resultName}.turnId`,
|
|
721
726
|
{ maxLength: 256 },
|
|
722
727
|
),
|
|
723
728
|
updatedAt: requireTimestamp(
|
|
724
|
-
|
|
725
|
-
`${
|
|
729
|
+
result.updatedAt,
|
|
730
|
+
`${resultName}.updatedAt`,
|
|
726
731
|
),
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
732
|
+
};
|
|
733
|
+
if (normalizedResult.summary === null) {
|
|
734
|
+
throw new Error(`${resultName}.summary must be a non-empty string`);
|
|
735
|
+
}
|
|
736
|
+
return normalizedResult;
|
|
737
|
+
};
|
|
738
|
+
let results = [];
|
|
739
|
+
if (dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION) {
|
|
740
|
+
if (!Array.isArray(dispatch.results)) throw new Error(`${name}.results must be an array`);
|
|
741
|
+
results = dispatch.results.map((result, index) => (
|
|
742
|
+
normalizeResult(result, `${name}.results[${index}]`)
|
|
743
|
+
));
|
|
744
|
+
} else if (dispatch.schemaVersion === PREVIOUS_TASK_SCHEMA_VERSION) {
|
|
745
|
+
if (dispatch.lastResult !== null) {
|
|
746
|
+
results = [normalizeResult(dispatch.lastResult, `${name}.lastResult`)];
|
|
731
747
|
}
|
|
732
748
|
} else if (RESULT_STATUSES.has(status)) {
|
|
733
|
-
|
|
749
|
+
results = [{ status, summary, turnId, updatedAt }];
|
|
734
750
|
}
|
|
751
|
+
const lastResult = results.at(-1) ?? null;
|
|
735
752
|
const normalized = {
|
|
736
753
|
schemaVersion: dispatch.schemaVersion,
|
|
737
754
|
id,
|
|
@@ -747,22 +764,25 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
747
764
|
turnId,
|
|
748
765
|
updatedAt,
|
|
749
766
|
updatedBy: requireEnum(dispatch.updatedBy, TASK_UPDATE_SOURCES, `${name}.updatedBy`),
|
|
767
|
+
results,
|
|
750
768
|
lastResult,
|
|
751
769
|
};
|
|
752
770
|
const isSelfLinkingRecord = normalized.threadId !== null
|
|
753
771
|
&& parseTaskChefMarker(normalized.instruction) === normalized.id;
|
|
754
772
|
if (isSelfLinkingRecord) {
|
|
755
773
|
if (normalized.turnId !== null) {
|
|
756
|
-
normalizeCodexThreadId(normalized.turnId, `${name}.turnId`);
|
|
774
|
+
normalized.turnId = normalizeCodexThreadId(normalized.turnId, `${name}.turnId`);
|
|
757
775
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
776
|
+
for (const [index, result] of normalized.results.entries()) {
|
|
777
|
+
if (result.turnId != null) {
|
|
778
|
+
result.turnId = normalizeCodexThreadId(
|
|
779
|
+
result.turnId,
|
|
780
|
+
`${name}.results[${index}].turnId`,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
763
783
|
}
|
|
764
784
|
}
|
|
765
|
-
if (normalized.schemaVersion >=
|
|
785
|
+
if (normalized.schemaVersion >= FIRST_SELF_LINKING_TASK_SCHEMA_VERSION) {
|
|
766
786
|
if (normalized.threadId === null) {
|
|
767
787
|
const isLinkPending = normalized.status === "working"
|
|
768
788
|
&& normalized.summary === null
|
|
@@ -782,8 +802,9 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
782
802
|
if (RESULT_STATUSES.has(normalized.status) && normalized.turnId === null) {
|
|
783
803
|
throw new Error(`${name}.turnId is required for a linked semantic state`);
|
|
784
804
|
}
|
|
785
|
-
|
|
786
|
-
|
|
805
|
+
const resultWithoutTurn = normalized.results.findIndex((result) => result.turnId === null);
|
|
806
|
+
if (resultWithoutTurn !== -1) {
|
|
807
|
+
throw new Error(`${name}.results[${resultWithoutTurn}].turnId is required for a linked result`);
|
|
787
808
|
}
|
|
788
809
|
}
|
|
789
810
|
}
|
|
@@ -793,7 +814,7 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
793
814
|
if (RESULT_STATUSES.has(normalized.status) && normalized.summary === null) {
|
|
794
815
|
throw new Error(`${name}.summary is required for status ${normalized.status}`);
|
|
795
816
|
}
|
|
796
|
-
if (normalized.schemaVersion >=
|
|
817
|
+
if (normalized.schemaVersion >= PREVIOUS_TASK_SCHEMA_VERSION) {
|
|
797
818
|
if (RESULT_STATUSES.has(normalized.status)) {
|
|
798
819
|
if (
|
|
799
820
|
normalized.lastResult === null
|
|
@@ -829,6 +850,34 @@ async function validateDispatchShape(dispatch, name = "task") {
|
|
|
829
850
|
throw new Error(`${name}.turnId must be newer than lastResult.turnId while working`);
|
|
830
851
|
}
|
|
831
852
|
}
|
|
853
|
+
const seenResultTurns = new Set();
|
|
854
|
+
const nullTurnKey = Symbol("null turn");
|
|
855
|
+
for (const [index, result] of normalized.results.entries()) {
|
|
856
|
+
const turnKey = result.turnId ?? nullTurnKey;
|
|
857
|
+
if (seenResultTurns.has(turnKey)) {
|
|
858
|
+
throw new Error(`${name}.results contains duplicate turnId: ${result.turnId ?? "null"}`);
|
|
859
|
+
}
|
|
860
|
+
seenResultTurns.add(turnKey);
|
|
861
|
+
if (Date.parse(result.updatedAt) < Date.parse(normalized.createdAt)) {
|
|
862
|
+
throw new Error(`${name}.results[${index}].updatedAt must not be earlier than createdAt`);
|
|
863
|
+
}
|
|
864
|
+
if (Date.parse(result.updatedAt) > Date.parse(normalized.updatedAt)) {
|
|
865
|
+
throw new Error(`${name}.results[${index}].updatedAt must not be later than updatedAt`);
|
|
866
|
+
}
|
|
867
|
+
if (
|
|
868
|
+
index > 0
|
|
869
|
+
&& Date.parse(result.updatedAt) < Date.parse(normalized.results[index - 1].updatedAt)
|
|
870
|
+
) {
|
|
871
|
+
throw new Error(`${name}.results must be ordered by updatedAt`);
|
|
872
|
+
}
|
|
873
|
+
if (
|
|
874
|
+
isSelfLinkingRecord
|
|
875
|
+
&& index > 0
|
|
876
|
+
&& result.turnId <= normalized.results[index - 1].turnId
|
|
877
|
+
) {
|
|
878
|
+
throw new Error(`${name}.results must be ordered by turnId`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
832
881
|
if (
|
|
833
882
|
Date.parse(normalized.updatedAt) < Date.parse(normalized.createdAt)
|
|
834
883
|
) {
|
|
@@ -911,6 +960,70 @@ export async function parseTaskLogContent(workspaceRoot, content) {
|
|
|
911
960
|
return (await parseDispatchRecordsUnlocked(root, content)).map((record) => record.normalized);
|
|
912
961
|
}
|
|
913
962
|
|
|
963
|
+
function schema6Task(dispatch, patch = {}) {
|
|
964
|
+
const { lastResult: _lastResult, ...persisted } = dispatch;
|
|
965
|
+
return {
|
|
966
|
+
...persisted,
|
|
967
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
968
|
+
results: dispatch.results ?? [],
|
|
969
|
+
...patch,
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
export async function migrateTaskLog(workspaceRoot, {
|
|
974
|
+
now = () => new Date().toISOString(),
|
|
975
|
+
writeTaskLog = writeTextAtomic,
|
|
976
|
+
} = {}) {
|
|
977
|
+
const root = await realpath(path.resolve(workspaceRoot));
|
|
978
|
+
return withWorkspaceLock(root, async () => {
|
|
979
|
+
const dispatchPath = path.join(root, DISPATCH_FILE_NAME);
|
|
980
|
+
if (!(await managedRegularFileExists(dispatchPath))) {
|
|
981
|
+
throw new Error(`task log does not exist: ${dispatchPath}`);
|
|
982
|
+
}
|
|
983
|
+
const original = await readFile(dispatchPath, "utf8");
|
|
984
|
+
const records = await parseDispatchRecordsUnlocked(root, original);
|
|
985
|
+
const migratedCount = records.filter(
|
|
986
|
+
(record) => record.raw.schemaVersion !== CURRENT_TASK_SCHEMA_VERSION,
|
|
987
|
+
).length;
|
|
988
|
+
if (migratedCount === 0) {
|
|
989
|
+
return {
|
|
990
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
991
|
+
action: "unchanged",
|
|
992
|
+
taskCount: records.length,
|
|
993
|
+
migratedCount: 0,
|
|
994
|
+
backupPath: null,
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
const lines = records.map((record) => JSON.stringify(schema6Task(record.normalized)));
|
|
998
|
+
const migrated = lines.length === 0 ? "" : `${lines.join("\n")}\n`;
|
|
999
|
+
await parseDispatchRecordsUnlocked(root, migrated);
|
|
1000
|
+
const timestamp = requireTimestamp(now(), "migration timestamp")
|
|
1001
|
+
.replaceAll(":", "-")
|
|
1002
|
+
.replaceAll(".", "-");
|
|
1003
|
+
const backupPath = `${dispatchPath}.pre-v6-${timestamp}-${randomUUID()}.bak`;
|
|
1004
|
+
await writeFile(backupPath, original, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
1005
|
+
if (await readFile(backupPath, "utf8") !== original) {
|
|
1006
|
+
throw new Error(`task log backup validation failed: ${backupPath}`);
|
|
1007
|
+
}
|
|
1008
|
+
try {
|
|
1009
|
+
await writeTaskLog(dispatchPath, migrated);
|
|
1010
|
+
await parseDispatchRecordsUnlocked(root, await readFile(dispatchPath, "utf8"));
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
`task log migration failed after recovery backup ${backupPath}: ${error.message}`,
|
|
1014
|
+
{ cause: error },
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
1019
|
+
action: "migrated",
|
|
1020
|
+
taskCount: records.length,
|
|
1021
|
+
migratedCount,
|
|
1022
|
+
backupPath,
|
|
1023
|
+
};
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
|
|
914
1027
|
export async function recordTask(workspaceRoot, input, { now } = {}) {
|
|
915
1028
|
requireExactFields(input, RECORD_DISPATCH_FIELDS, "task input");
|
|
916
1029
|
const root = await realpath(path.resolve(workspaceRoot));
|
|
@@ -933,7 +1046,7 @@ export async function recordTask(workspaceRoot, input, { now } = {}) {
|
|
|
933
1046
|
turnId: null,
|
|
934
1047
|
updatedAt: createdAt,
|
|
935
1048
|
updatedBy: "dispatcher",
|
|
936
|
-
|
|
1049
|
+
results: [],
|
|
937
1050
|
});
|
|
938
1051
|
const existing = await readDispatchesUnlocked(root);
|
|
939
1052
|
if (existing.some((item) => item.id === dispatch.id)) {
|
|
@@ -975,13 +1088,11 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
975
1088
|
throw new Error(`task instruction does not contain its exact TaskChef marker: ${id}`);
|
|
976
1089
|
}
|
|
977
1090
|
if (dispatch.threadId === durableThreadId) return dispatch;
|
|
978
|
-
const canonical = await validateDispatchShape({
|
|
979
|
-
...dispatch,
|
|
980
|
-
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
1091
|
+
const canonical = await validateDispatchShape(schema6Task(dispatch, {
|
|
981
1092
|
threadId: durableThreadId,
|
|
982
1093
|
updatedAt: transitionTimestamp(now, dispatch.updatedAt),
|
|
983
1094
|
updatedBy: "mcp",
|
|
984
|
-
});
|
|
1095
|
+
}));
|
|
985
1096
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
986
1097
|
? dispatchLineWithState(canonical, {})
|
|
987
1098
|
: record.line);
|
|
@@ -1006,13 +1117,11 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
1006
1117
|
))) {
|
|
1007
1118
|
throw new Error(`threadId is already recorded: ${durableThreadId}`);
|
|
1008
1119
|
}
|
|
1009
|
-
const linked = await validateDispatchShape({
|
|
1010
|
-
...dispatch,
|
|
1011
|
-
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
1120
|
+
const linked = await validateDispatchShape(schema6Task(dispatch, {
|
|
1012
1121
|
threadId: durableThreadId,
|
|
1013
1122
|
updatedAt: transitionTimestamp(now, dispatch.updatedAt),
|
|
1014
1123
|
updatedBy: "mcp",
|
|
1015
|
-
});
|
|
1124
|
+
}));
|
|
1016
1125
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
1017
1126
|
? dispatchLineWithState(linked, {})
|
|
1018
1127
|
: record.line);
|
|
@@ -1022,11 +1131,7 @@ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
|
|
|
1022
1131
|
}
|
|
1023
1132
|
|
|
1024
1133
|
function dispatchLineWithState(dispatch, patch) {
|
|
1025
|
-
return JSON.stringify(
|
|
1026
|
-
...dispatch,
|
|
1027
|
-
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
1028
|
-
...patch,
|
|
1029
|
-
});
|
|
1134
|
+
return JSON.stringify(schema6Task(dispatch, patch));
|
|
1030
1135
|
}
|
|
1031
1136
|
|
|
1032
1137
|
function normalizeTaskStateInput(input, { allowWorking }) {
|
|
@@ -1108,7 +1213,7 @@ async function reportTaskStateInternal(
|
|
|
1108
1213
|
if (!compatibilityAlias) {
|
|
1109
1214
|
const rawSchemaVersion = records[index].raw.schemaVersion;
|
|
1110
1215
|
const hasCurrentMarker = rawSchemaVersion
|
|
1111
|
-
>=
|
|
1216
|
+
>= FIRST_SELF_LINKING_TASK_SCHEMA_VERSION
|
|
1112
1217
|
&& parseTaskChefMarker(dispatch.instruction) === dispatch.id;
|
|
1113
1218
|
const isFreshCreationFailure = hasCurrentMarker
|
|
1114
1219
|
&& dispatch.status === "working"
|
|
@@ -1147,31 +1252,24 @@ async function reportTaskStateInternal(
|
|
|
1147
1252
|
}
|
|
1148
1253
|
}
|
|
1149
1254
|
const updatedAt = transitionTimestamp(now, dispatch.updatedAt);
|
|
1150
|
-
const updated = await validateDispatchShape({
|
|
1151
|
-
...dispatch,
|
|
1152
|
-
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
1255
|
+
const updated = await validateDispatchShape(schema6Task(dispatch, {
|
|
1153
1256
|
status,
|
|
1154
1257
|
summary: null,
|
|
1155
1258
|
turnId: stateTurnId,
|
|
1156
1259
|
updatedAt,
|
|
1157
1260
|
updatedBy: "mcp",
|
|
1158
|
-
|
|
1159
|
-
});
|
|
1261
|
+
}));
|
|
1160
1262
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
1161
1263
|
? dispatchLineWithState(updated, {})
|
|
1162
1264
|
: record.line);
|
|
1163
1265
|
await writeDispatchLinesAtomic(root, lines);
|
|
1164
1266
|
return updated;
|
|
1165
1267
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
&& dispatch.turnId === stateTurnId
|
|
1169
|
-
&& dispatch.summary === summary
|
|
1170
|
-
&& sameLastResult(dispatch.lastResult, { status, summary, turnId: stateTurnId })
|
|
1171
|
-
) {
|
|
1268
|
+
const priorTurnResult = dispatch.results.find((result) => result.turnId === stateTurnId);
|
|
1269
|
+
if (priorTurnResult && sameLastResult(priorTurnResult, { status, summary, turnId: stateTurnId })) {
|
|
1172
1270
|
return dispatch;
|
|
1173
1271
|
}
|
|
1174
|
-
if (dispatch.turnId === stateTurnId && dispatch.status !== "working") {
|
|
1272
|
+
if (priorTurnResult || (dispatch.turnId === stateTurnId && dispatch.status !== "working")) {
|
|
1175
1273
|
throw new Error(`task turn already has a different semantic result: ${id}`);
|
|
1176
1274
|
}
|
|
1177
1275
|
if (compatibilityAlias) {
|
|
@@ -1190,16 +1288,14 @@ async function reportTaskStateInternal(
|
|
|
1190
1288
|
}
|
|
1191
1289
|
const updatedAt = transitionTimestamp(now, dispatch.updatedAt);
|
|
1192
1290
|
const lastResult = { status, summary, turnId: stateTurnId, updatedAt };
|
|
1193
|
-
const candidate = {
|
|
1194
|
-
...dispatch,
|
|
1195
|
-
schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
|
|
1291
|
+
const candidate = schema6Task(dispatch, {
|
|
1196
1292
|
status,
|
|
1197
1293
|
summary,
|
|
1198
1294
|
turnId: stateTurnId,
|
|
1199
1295
|
updatedAt,
|
|
1200
1296
|
updatedBy: "mcp",
|
|
1201
|
-
lastResult,
|
|
1202
|
-
};
|
|
1297
|
+
results: [...dispatch.results, lastResult],
|
|
1298
|
+
});
|
|
1203
1299
|
const updated = await validateDispatchShape(candidate);
|
|
1204
1300
|
const lines = records.map((record, recordIndex) => recordIndex === index
|
|
1205
1301
|
? dispatchLineWithState(updated, {})
|