taskchef 5.11.2 → 6.0.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/src/workspace.js CHANGED
@@ -20,6 +20,7 @@ import path from "node:path";
20
20
  import { promisify } from "node:util";
21
21
  import lockfile from "proper-lockfile";
22
22
  import {
23
+ normalizeCodexThreadId,
23
24
  normalizeDurableThreadId,
24
25
  parseTaskChefMarker,
25
26
  taskChefMarker,
@@ -47,7 +48,8 @@ const DISPATCH_FILE_NAME = "tasks.jsonl";
47
48
  const WORKSPACE_LOCK_NAME = ".taskchef-workspace.lock";
48
49
  const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
49
50
  const CURRENT_CONFIG_SCHEMA_VERSION = 2;
50
- const CURRENT_TASK_SCHEMA_VERSION = 3;
51
+ const CURRENT_TASK_SCHEMA_VERSION = 4;
52
+ const PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION = 3;
51
53
  const LEGACY_SCHEMA_VERSION = 1;
52
54
  const PREVIOUS_SCHEMA_VERSION = 2;
53
55
  const CONFIG_FIELDS = new Set(["schemaVersion", "projects"]);
@@ -786,6 +788,7 @@ async function validateDispatchShape(
786
788
  const supportedVersions = [
787
789
  LEGACY_SCHEMA_VERSION,
788
790
  PREVIOUS_SCHEMA_VERSION,
791
+ PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION,
789
792
  CURRENT_TASK_SCHEMA_VERSION,
790
793
  ];
791
794
  if (!supportedVersions.includes(dispatch?.schemaVersion)) {
@@ -793,7 +796,7 @@ async function validateDispatchShape(
793
796
  }
794
797
  requireExactFields(
795
798
  dispatch,
796
- dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
799
+ dispatch.schemaVersion >= PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION
797
800
  ? DISPATCH_FIELDS
798
801
  : LEGACY_DISPATCH_FIELDS,
799
802
  name,
@@ -804,7 +807,7 @@ async function validateDispatchShape(
804
807
  allowLegacyGithubRepo: dispatch.schemaVersion === LEGACY_SCHEMA_VERSION,
805
808
  });
806
809
  const normalized = {
807
- schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
810
+ schemaVersion: dispatch.schemaVersion,
808
811
  id,
809
812
  project,
810
813
  title: requireString(dispatch.title, `${name}.title`).trim(),
@@ -813,21 +816,21 @@ async function validateDispatchShape(
813
816
  ? null
814
817
  : normalizeDurableThreadId(dispatch.threadId, `${name}.threadId`),
815
818
  createdAt: requireTimestamp(dispatch.createdAt, `${name}.createdAt`),
816
- status: dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
819
+ status: dispatch.schemaVersion >= PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION
817
820
  ? requireEnum(dispatch.status, TASK_STATUSES, `${name}.status`)
818
821
  : null,
819
- summary: dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
822
+ summary: dispatch.schemaVersion >= PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION
820
823
  ? optionalString(dispatch.summary, `${name}.summary`, {
821
824
  maxLength: MAX_RESULT_SUMMARY_LENGTH,
822
825
  })
823
826
  : null,
824
- turnId: dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
827
+ turnId: dispatch.schemaVersion >= PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION
825
828
  ? optionalString(dispatch.turnId, `${name}.turnId`, { maxLength: 256 })
826
829
  : null,
827
- updatedAt: dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
830
+ updatedAt: dispatch.schemaVersion >= PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION
828
831
  ? requireTimestamp(dispatch.updatedAt, `${name}.updatedAt`)
829
832
  : null,
830
- updatedBy: dispatch.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
833
+ updatedBy: dispatch.schemaVersion >= PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION
831
834
  ? requireEnum(dispatch.updatedBy, TASK_UPDATE_SOURCES, `${name}.updatedBy`)
832
835
  : null,
833
836
  };
@@ -881,15 +884,24 @@ async function parseDispatchRecordsUnlocked(root, content) {
881
884
  const threadIds = new Set();
882
885
  for (const { normalized: dispatch } of records) {
883
886
  if (ids.has(dispatch.id)) throw new Error(`duplicate task ID: ${dispatch.id}`);
884
- if (dispatch.threadId !== null && threadIds.has(dispatch.threadId)) {
887
+ const threadKey = dispatch.threadId === null ? null : threadIdentityKey(dispatch.threadId);
888
+ if (threadKey !== null && threadIds.has(threadKey)) {
885
889
  throw new Error(`duplicate task threadId: ${dispatch.threadId}`);
886
890
  }
887
891
  ids.add(dispatch.id);
888
- if (dispatch.threadId !== null) threadIds.add(dispatch.threadId);
892
+ if (threadKey !== null) threadIds.add(threadKey);
889
893
  }
890
894
  return records;
891
895
  }
892
896
 
897
+ function threadIdentityKey(threadId) {
898
+ try {
899
+ return `codex:${normalizeCodexThreadId(threadId)}`;
900
+ } catch {
901
+ return `opaque:${threadId}`;
902
+ }
903
+ }
904
+
893
905
  async function readDispatchRecordsUnlocked(root) {
894
906
  const filePath = path.join(root, DISPATCH_FILE_NAME);
895
907
  if (!(await managedRegularFileExists(filePath))) {
@@ -942,10 +954,19 @@ export async function recordTask(workspaceRoot, input, { now } = {}) {
942
954
  }
943
955
  if (
944
956
  dispatch.threadId !== null &&
945
- existing.some((item) => item.threadId === dispatch.threadId)
957
+ existing.some((item) => (
958
+ item.threadId !== null
959
+ && threadIdentityKey(item.threadId) === threadIdentityKey(dispatch.threadId)
960
+ ))
946
961
  ) {
947
962
  throw new Error(`threadId is already recorded: ${dispatch.threadId}`);
948
963
  }
964
+ if (
965
+ dispatch.threadId !== null
966
+ && parseTaskChefMarker(dispatch.instruction) === dispatch.id
967
+ ) {
968
+ throw new Error("a marked self-linking task must be recorded with threadId: null");
969
+ }
949
970
  await appendDispatchesAtomic(root, [dispatch]);
950
971
  return dispatch;
951
972
  });
@@ -960,6 +981,10 @@ export async function resolveTask(workspaceRoot, taskId, threadId, { now } = {})
960
981
  const dispatches = records.map((record) => record.normalized);
961
982
  const index = dispatches.findIndex((dispatch) => dispatch.id === id);
962
983
  if (index === -1) throw new Error(`task not found: ${id}`);
984
+ const currentRecord = records[index];
985
+ if (currentRecord.raw.schemaVersion >= CURRENT_TASK_SCHEMA_VERSION) {
986
+ throw new Error(`task resolve is only available for legacy pre-self-linking records: ${id}`);
987
+ }
963
988
  const dispatch = dispatches[index];
964
989
  if (dispatch.threadId === durableThreadId) return dispatch;
965
990
  if (dispatch.threadId !== null) {
@@ -968,86 +993,101 @@ export async function resolveTask(workspaceRoot, taskId, threadId, { now } = {})
968
993
  if (parseTaskChefMarker(dispatch.instruction) !== dispatch.id) {
969
994
  throw new Error(`task instruction does not contain its exact TaskChef marker: ${id}`);
970
995
  }
971
- if (dispatches.some((item) => item.threadId === durableThreadId)) {
996
+ if (dispatches.some((item) => (
997
+ item.threadId !== null
998
+ && threadIdentityKey(item.threadId) === threadIdentityKey(durableThreadId)
999
+ ))) {
972
1000
  throw new Error(`threadId is already recorded: ${durableThreadId}`);
973
1001
  }
974
- const currentRecord = records[index];
975
- const resolved = currentRecord.raw.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
976
- ? await validateDispatchShape({
977
- ...dispatch,
1002
+ const statefulLegacy = currentRecord.raw.schemaVersion === PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION;
1003
+ const rawResolved = statefulLegacy
1004
+ ? {
1005
+ ...currentRecord.raw,
978
1006
  threadId: durableThreadId,
979
1007
  updatedAt: now ?? new Date().toISOString(),
980
1008
  updatedBy: "dispatcher",
981
- })
1009
+ }
1010
+ : { ...currentRecord.raw, threadId: durableThreadId };
1011
+ const resolved = statefulLegacy
1012
+ ? await validateDispatchShape(rawResolved)
982
1013
  : { ...dispatch, threadId: durableThreadId };
983
1014
  const lines = records.map((record, recordIndex) => recordIndex === index
984
- ? currentRecord.raw.schemaVersion === CURRENT_TASK_SCHEMA_VERSION
985
- ? dispatchLineWithState(resolved, {})
986
- : JSON.stringify({ ...record.raw, threadId: durableThreadId })
1015
+ ? JSON.stringify(rawResolved)
987
1016
  : record.line);
988
1017
  await writeDispatchLinesAtomic(root, lines);
989
1018
  return resolved;
990
1019
  });
991
1020
  }
992
1021
 
993
- function dispatchLineWithState(dispatch, patch) {
994
- return JSON.stringify({
995
- ...dispatch,
996
- schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
997
- ...patch,
998
- });
999
- }
1000
-
1001
- export async function startTaskFromHook(
1002
- workspaceRoot,
1003
- taskId,
1004
- threadId,
1005
- turnId,
1006
- { now } = {},
1007
- ) {
1022
+ export async function linkTask(workspaceRoot, taskId, threadId, { now } = {}) {
1008
1023
  const id = requireSafeId(taskId, "taskId");
1009
- const durableThreadId = normalizeDurableThreadId(threadId);
1010
- const currentTurnId = optionalString(turnId, "turnId", { maxLength: 256 });
1024
+ const durableThreadId = normalizeCodexThreadId(threadId);
1011
1025
  const root = await realpath(path.resolve(workspaceRoot));
1012
1026
  return withWorkspaceLock(root, async () => {
1013
1027
  const records = await readDispatchRecordsUnlocked(root);
1014
1028
  const dispatches = records.map((record) => record.normalized);
1015
1029
  const index = dispatches.findIndex((dispatch) => dispatch.id === id);
1016
1030
  if (index === -1) throw new Error(`task not found: ${id}`);
1031
+ const currentRecord = records[index];
1032
+ if (currentRecord.raw.schemaVersion < CURRENT_TASK_SCHEMA_VERSION) {
1033
+ throw new Error(`link_task accepts only self-linking task records: ${id}`);
1034
+ }
1017
1035
  const dispatch = dispatches[index];
1018
- if (parseTaskChefMarker(dispatch.instruction) !== dispatch.id) {
1019
- throw new Error(`task instruction does not contain its exact TaskChef marker: ${id}`);
1036
+ const sameIdentity = dispatch.threadId?.toLowerCase() === durableThreadId;
1037
+ if (sameIdentity && dispatch.updatedBy === "mcp") {
1038
+ if (parseTaskChefMarker(dispatch.instruction) !== dispatch.id) {
1039
+ throw new Error(`task instruction does not contain its exact TaskChef marker: ${id}`);
1040
+ }
1041
+ if (dispatch.threadId === durableThreadId) return dispatch;
1042
+ const canonical = await validateDispatchShape({
1043
+ ...dispatch,
1044
+ threadId: durableThreadId,
1045
+ updatedAt: now ?? new Date().toISOString(),
1046
+ updatedBy: "mcp",
1047
+ });
1048
+ const lines = records.map((record, recordIndex) => recordIndex === index
1049
+ ? dispatchLineWithState(canonical, {})
1050
+ : record.line);
1051
+ await writeDispatchLinesAtomic(root, lines);
1052
+ return canonical;
1020
1053
  }
1021
- if (dispatch.threadId !== null && dispatch.threadId !== durableThreadId) {
1054
+ if (dispatch.threadId !== null && dispatch.updatedBy === "mcp") {
1022
1055
  throw new Error(`task already has a different threadId: ${id}`);
1023
1056
  }
1024
1057
  if (
1025
- dispatch.threadId === null
1026
- && dispatches.some((item) => item.id !== id && item.threadId === durableThreadId)
1058
+ dispatch.threadId !== null
1059
+ || dispatch.status !== "working"
1060
+ || dispatch.updatedBy !== "dispatcher"
1027
1061
  ) {
1028
- throw new Error(`threadId is already recorded: ${durableThreadId}`);
1062
+ throw new Error(`task is not an eligible link-pending dispatcher record: ${id}`);
1029
1063
  }
1030
- if (dispatch.threadId === durableThreadId && dispatch.updatedBy === "mcp") {
1031
- return dispatch;
1064
+ if (parseTaskChefMarker(dispatch.instruction) !== dispatch.id) {
1065
+ throw new Error(`task instruction does not contain its exact TaskChef marker: ${id}`);
1032
1066
  }
1033
- if (dispatch.threadId === durableThreadId && dispatch.updatedBy === "hook") {
1034
- return dispatch;
1067
+ if (dispatches.some((item) => (
1068
+ item.id !== id && item.threadId?.toLowerCase() === durableThreadId
1069
+ ))) {
1070
+ throw new Error(`threadId is already recorded: ${durableThreadId}`);
1035
1071
  }
1036
- const updatedAt = now ?? new Date().toISOString();
1037
- const started = await validateDispatchShape({
1072
+ const linked = await validateDispatchShape({
1038
1073
  ...dispatch,
1039
1074
  threadId: durableThreadId,
1040
- status: "working",
1041
- summary: null,
1042
- turnId: currentTurnId,
1043
- updatedAt,
1044
- updatedBy: "hook",
1075
+ updatedAt: now ?? new Date().toISOString(),
1076
+ updatedBy: "mcp",
1045
1077
  });
1046
1078
  const lines = records.map((record, recordIndex) => recordIndex === index
1047
- ? dispatchLineWithState(started, {})
1079
+ ? dispatchLineWithState(linked, {})
1048
1080
  : record.line);
1049
1081
  await writeDispatchLinesAtomic(root, lines);
1050
- return started;
1082
+ return linked;
1083
+ });
1084
+ }
1085
+
1086
+ function dispatchLineWithState(dispatch, patch) {
1087
+ return JSON.stringify({
1088
+ ...dispatch,
1089
+ schemaVersion: CURRENT_TASK_SCHEMA_VERSION,
1090
+ ...patch,
1051
1091
  });
1052
1092
  }
1053
1093
 
@@ -1074,28 +1114,55 @@ export async function reportTaskResult(workspaceRoot, input, { now } = {}) {
1074
1114
  const index = dispatches.findIndex((dispatch) => dispatch.id === id);
1075
1115
  if (index === -1) throw new Error(`task not found: ${id}`);
1076
1116
  const dispatch = dispatches[index];
1117
+ const isSelfLinkingJourney = (
1118
+ records[index].raw.schemaVersion >= CURRENT_TASK_SCHEMA_VERSION
1119
+ && dispatch.threadId !== null
1120
+ && parseTaskChefMarker(dispatch.instruction) === dispatch.id
1121
+ );
1122
+ let resultTurnId = turnId;
1123
+ if (isSelfLinkingJourney) {
1124
+ if (dispatch.updatedBy === "dispatcher") {
1125
+ throw new Error(`self-linking task must link before reporting a result: ${id}`);
1126
+ }
1127
+ resultTurnId = normalizeCodexThreadId(turnId, "turnId");
1128
+ }
1077
1129
  if (dispatch.threadId === null) {
1078
- if (threadId !== null || turnId !== null || status !== "failed") {
1130
+ if (threadId !== null || resultTurnId !== null || status !== "failed") {
1079
1131
  throw new Error(`task without a durable threadId accepts only failed with null thread/turn IDs: ${id}`);
1080
1132
  }
1081
1133
  } else {
1082
- if (threadId !== dispatch.threadId) {
1134
+ if (threadIdentityKey(threadId) !== threadIdentityKey(dispatch.threadId)) {
1083
1135
  throw new Error(`task result threadId does not match recorded threadId: ${id}`);
1084
1136
  }
1085
- if (turnId === null) {
1137
+ if (resultTurnId === null) {
1086
1138
  throw new Error(`task result turnId is required for a linked task: ${id}`);
1087
1139
  }
1088
1140
  }
1141
+ if (dispatch.updatedBy === "mcp" && resultTurnId === dispatch.turnId) {
1142
+ if (status === dispatch.status && summary === dispatch.summary) return dispatch;
1143
+ throw new Error(`task turn already has a different semantic result: ${id}`);
1144
+ }
1145
+ if (
1146
+ isSelfLinkingJourney
1147
+ && dispatch.turnId !== null
1148
+ && resultTurnId <= dispatch.turnId
1149
+ ) {
1150
+ throw new Error(`task result turnId must be newer than the stored turnId: ${id}`);
1151
+ }
1152
+ const persistedSchemaVersion = records[index].raw.schemaVersion >= CURRENT_TASK_SCHEMA_VERSION
1153
+ ? CURRENT_TASK_SCHEMA_VERSION
1154
+ : PREVIOUS_STATEFUL_TASK_SCHEMA_VERSION;
1089
1155
  const updated = await validateDispatchShape({
1090
1156
  ...dispatch,
1157
+ schemaVersion: persistedSchemaVersion,
1091
1158
  status,
1092
1159
  summary,
1093
- turnId,
1160
+ turnId: resultTurnId,
1094
1161
  updatedAt: now ?? new Date().toISOString(),
1095
1162
  updatedBy: "mcp",
1096
1163
  });
1097
1164
  const lines = records.map((record, recordIndex) => recordIndex === index
1098
- ? dispatchLineWithState(updated, {})
1165
+ ? dispatchLineWithState(updated, { schemaVersion: persistedSchemaVersion })
1099
1166
  : record.line);
1100
1167
  await writeDispatchLinesAtomic(root, lines);
1101
1168
  return updated;
package/hooks/hooks.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "description": "Link TaskChef executor identity and provide the current callback turn.",
3
- "hooks": {
4
- "UserPromptSubmit": [
5
- {
6
- "hooks": [
7
- {
8
- "type": "command",
9
- "command": "node \"$PLUGIN_ROOT/hooks/taskchef-initial-prompt.js\"",
10
- "commandWindows": "node \"%PLUGIN_ROOT%\\hooks\\taskchef-initial-prompt.js\"",
11
- "timeout": 40,
12
- "statusMessage": "Linking TaskChef task"
13
- }
14
- ]
15
- }
16
- ]
17
- }
18
- }
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { handleInitialPromptHook } from "../src/hook.js";
4
-
5
- let raw = "";
6
- process.stdin.setEncoding("utf8");
7
- for await (const chunk of process.stdin) raw += chunk;
8
-
9
- try {
10
- const input = JSON.parse(raw);
11
- process.stdout.write(`${JSON.stringify(await handleInitialPromptHook(input))}\n`);
12
- } catch (error) {
13
- process.stdout.write(`${JSON.stringify({
14
- continue: true,
15
- systemMessage: `TaskChef initial identity hook failed: ${error instanceof Error ? error.message : String(error)}`,
16
- })}\n`);
17
- }
package/src/hook.js DELETED
@@ -1,96 +0,0 @@
1
- import { parseTaskChefMarker } from "./delegation.js";
2
- import { listTasks, startTaskFromHook } from "./workspace.js";
3
- import { resolveWorkspacePath } from "./workspace-path.js";
4
-
5
- // The final hook check trails the dispatcher's 30-second resolver checkpoint
6
- // so a simultaneous final match is visible before the hook fails closed.
7
- export const INITIAL_LINK_CHECKPOINTS_MS = Object.freeze([0, 250, 1_000, 3_000, 10_000, 32_000]);
8
-
9
- function nonEmptyString(value) {
10
- return typeof value === "string" && value.trim().length > 0
11
- ? value.trim()
12
- : null;
13
- }
14
-
15
- function hookContext(taskId, threadId, turnId) {
16
- return {
17
- continue: true,
18
- hookSpecificOutput: {
19
- hookEventName: "UserPromptSubmit",
20
- additionalContext: [
21
- `This is TaskChef task ${taskId} in root thread ${threadId}, current turn ${turnId}.`,
22
- "Before ending the task, call the TaskChef report_result MCP tool with the semantic outcome needs_input, completed, or failed and a concise summary.",
23
- "Pass this exact task ID, root thread ID, and current turn ID to report_result.",
24
- "Use needs_input only for a decision or information the user must provide; native approval prompts are reported from live Codex state.",
25
- "Do not include secrets, transcripts, or raw command output in the summary.",
26
- ].join(" "),
27
- },
28
- };
29
- }
30
-
31
- async function waitForLinkedTask(root, taskId, {
32
- checkpoints = INITIAL_LINK_CHECKPOINTS_MS,
33
- listTaskSnapshots = listTasks,
34
- waitImpl = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
35
- } = {}) {
36
- let previousCheckpoint = 0;
37
- for (const checkpoint of checkpoints) {
38
- if (!Number.isFinite(checkpoint) || checkpoint < previousCheckpoint) {
39
- throw new Error("initial-link checkpoints must be finite, non-decreasing milliseconds");
40
- }
41
- const delay = checkpoint - previousCheckpoint;
42
- if (delay > 0) await waitImpl(delay);
43
- previousCheckpoint = checkpoint;
44
- const task = (await listTaskSnapshots(root)).find((item) => item.id === taskId);
45
- if (task?.threadId) return task;
46
- }
47
- return null;
48
- }
49
-
50
- export async function handleInitialPromptHook(input, {
51
- workspace = null,
52
- resolveWorkspace = () => resolveWorkspacePath().workspace,
53
- startTask = startTaskFromHook,
54
- listTaskSnapshots = listTasks,
55
- linkCheckpoints = INITIAL_LINK_CHECKPOINTS_MS,
56
- waitImpl = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
57
- } = {}) {
58
- if (input?.hook_event_name !== "UserPromptSubmit") return { continue: true };
59
- const taskId = parseTaskChefMarker(input.prompt);
60
- const threadId = nonEmptyString(input.session_id);
61
- const turnId = nonEmptyString(input.turn_id);
62
- if (turnId === null || (taskId === null && threadId === null)) {
63
- if (taskId === null) return { continue: true };
64
- return {
65
- continue: true,
66
- systemMessage: "TaskChef could not prepare this initial task because the hook payload lacked a turn ID.",
67
- };
68
- }
69
- if (taskId !== null) {
70
- const root = workspace ?? resolveWorkspace();
71
- const linked = await waitForLinkedTask(root, taskId, {
72
- checkpoints: linkCheckpoints,
73
- listTaskSnapshots,
74
- waitImpl,
75
- });
76
- if (linked === null) {
77
- return {
78
- continue: true,
79
- systemMessage: "TaskChef left this task unresolved because its durable child task ID could not be verified. Do not report a semantic result until the recorded identity is repaired.",
80
- };
81
- }
82
- await startTask(root, taskId, linked.threadId, turnId);
83
- return hookContext(taskId, linked.threadId, turnId);
84
- }
85
-
86
- try {
87
- const root = workspace ?? resolveWorkspace();
88
- const task = (await listTaskSnapshots(root)).find((item) => item.threadId === threadId);
89
- return task === undefined
90
- ? { continue: true }
91
- : hookContext(task.id, threadId, turnId);
92
- } catch {
93
- // An unrelated prompt must not surface TaskChef workspace setup failures.
94
- return { continue: true };
95
- }
96
- }