@gajae-code/coding-agent 0.12.5 → 0.12.7

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +36 -1
  4. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  5. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  6. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  7. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  8. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  9. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  10. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  11. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  12. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  13. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  14. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  15. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
  16. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  17. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/sdk/broker/broker.d.ts +18 -0
  20. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  21. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  22. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
  23. package/dist/types/sdk/bus/index.d.ts +4 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  26. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  27. package/dist/types/session/agent-session.d.ts +5 -4
  28. package/dist/types/session/attempt-record-store.d.ts +19 -0
  29. package/dist/types/session/session-manager.d.ts +1 -0
  30. package/dist/types/session/session-storage.d.ts +8 -0
  31. package/dist/types/task/receipt.d.ts +1 -0
  32. package/dist/types/task/types.d.ts +43 -0
  33. package/dist/types/tools/subagent.d.ts +15 -0
  34. package/dist/types/tools/todo-write.d.ts +3 -0
  35. package/package.json +8 -8
  36. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  37. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  38. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  39. package/src/async/job-manager.ts +430 -46
  40. package/src/config/model-profiles.ts +8 -0
  41. package/src/config/settings-schema.ts +17 -3
  42. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  43. package/src/extensibility/extensions/runner.ts +80 -6
  44. package/src/extensibility/extensions/wrapper.ts +22 -15
  45. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  46. package/src/gjc-runtime/launch-tmux.ts +309 -293
  47. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  48. package/src/gjc-runtime/psmux-detect.ts +52 -6
  49. package/src/gjc-runtime/team-launch.ts +50 -2
  50. package/src/gjc-runtime/team-runtime.ts +716 -251
  51. package/src/gjc-runtime/team-store.ts +25 -7
  52. package/src/gjc-runtime/team-workers.ts +161 -1
  53. package/src/gjc-runtime/tmux-common.ts +16 -4
  54. package/src/gjc-runtime/tmux-gc.ts +37 -5
  55. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  56. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  57. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  58. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  59. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  60. package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
  61. package/src/internal-urls/docs-index.generated.ts +4 -4
  62. package/src/internal-urls/local-protocol.ts +39 -18
  63. package/src/modes/acp/acp-agent.ts +27 -13
  64. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  65. package/src/modes/components/plugin-settings.ts +5 -0
  66. package/src/modes/components/settings-selector.ts +4 -0
  67. package/src/modes/controllers/selector-controller.ts +1 -0
  68. package/src/prompts/tools/monitor.md +4 -4
  69. package/src/sdk/broker/broker.ts +93 -10
  70. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  71. package/src/sdk/broker/lifecycle.ts +691 -92
  72. package/src/sdk/bus/chat-daemon-control.ts +30 -6
  73. package/src/sdk/bus/index.ts +23 -6
  74. package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
  75. package/src/sdk/bus/telegram-daemon.ts +283 -44
  76. package/src/sdk/bus/topic-registry.ts +133 -10
  77. package/src/sdk/session.ts +7 -6
  78. package/src/session/agent-session.ts +752 -339
  79. package/src/session/attempt-record-store.ts +106 -0
  80. package/src/session/internal/managed-session-scope.ts +252 -67
  81. package/src/session/internal/managed-session-storage.ts +1 -0
  82. package/src/session/session-manager.ts +7 -0
  83. package/src/session/session-storage.ts +146 -24
  84. package/src/task/index.ts +230 -19
  85. package/src/task/provider-retry-status.ts +4 -1
  86. package/src/task/receipt.ts +2 -0
  87. package/src/task/types.ts +10 -0
  88. package/src/tools/monitor.ts +7 -1
  89. package/src/tools/output-meta.ts +10 -2
  90. package/src/tools/subagent.ts +97 -75
  91. package/src/tools/todo-write.ts +47 -0
package/src/task/index.ts CHANGED
@@ -88,6 +88,18 @@ import {
88
88
  type WorktreeBaseline,
89
89
  } from "./worktree";
90
90
 
91
+ interface DuplicateIdentity {
92
+ role: string;
93
+ ownerId?: string;
94
+ parentSession: string | null;
95
+ repository: { root: string; relativeSubdir: string | null };
96
+ }
97
+
98
+ interface DuplicateDisposition {
99
+ action: "warned" | "superseded";
100
+ predecessorIds: string[];
101
+ }
102
+
91
103
  interface TaskResumeDescriptor {
92
104
  toolCallId: string;
93
105
  params: TaskParams;
@@ -96,6 +108,11 @@ interface TaskResumeDescriptor {
96
108
  durableOutputAllowed?: boolean;
97
109
  forkContextSeed?: ForkContextSeed;
98
110
  agentSource: AgentDefinition["source"];
111
+ repositoryBinding: RepositoryBinding;
112
+ duplicateIdentity: DuplicateIdentity;
113
+ duplicatePolicy: "warn" | "supersede";
114
+ initialDisposition?: DuplicateDisposition;
115
+ lastAdmissionFailure?: string;
99
116
  }
100
117
 
101
118
  function isTaskResumeDescriptor(value: unknown): value is TaskResumeDescriptor {
@@ -185,18 +202,12 @@ async function resolveTaskItemsWithRepositoryBindings(
185
202
  for (const task of tasks) {
186
203
  try {
187
204
  const binding = await resolveTaskRepositoryBinding(cwd, task.repositoryBinding);
188
- if (binding.relativeSubdir) {
189
- assertPathUnderRepositoryBinding(binding, ".");
190
- }
191
- resolved.push({
192
- ...task,
193
- repositoryBinding: binding,
194
- });
205
+ if (binding.relativeSubdir) assertPathUnderRepositoryBinding(binding, ".");
206
+ resolved.push({ ...task, repositoryBinding: binding });
195
207
  } catch (error) {
196
208
  const id = task.id?.trim() ? task.id : "(missing-id)";
197
- if (error instanceof RepositoryBindingError) {
209
+ if (error instanceof RepositoryBindingError)
198
210
  return { tasks: [], error: `Task "${id}" repository binding rejected: ${error.message}` };
199
- }
200
211
  return {
201
212
  tasks: [],
202
213
  error: `Task "${id}" repository binding rejected: ${error instanceof Error ? error.message : String(error)}`,
@@ -206,6 +217,31 @@ async function resolveTaskItemsWithRepositoryBindings(
206
217
  return { tasks: resolved };
207
218
  }
208
219
 
220
+ function duplicateIdentityForTask(
221
+ task: TaskItem,
222
+ role: string,
223
+ ownerId: string | undefined,
224
+ parentSession: string | null,
225
+ ): DuplicateIdentity {
226
+ const binding = task.repositoryBinding as RepositoryBinding;
227
+ return {
228
+ role: role.trim(),
229
+ ownerId,
230
+ parentSession,
231
+ repository: { root: binding.commonDir ?? binding.worktreeRoot, relativeSubdir: binding.relativeSubdir ?? null },
232
+ };
233
+ }
234
+
235
+ function duplicateIdentityKey(identity: DuplicateIdentity): string {
236
+ return JSON.stringify([
237
+ identity.role,
238
+ identity.ownerId ?? null,
239
+ identity.parentSession,
240
+ identity.repository.root,
241
+ identity.repository.relativeSubdir,
242
+ ]);
243
+ }
244
+
209
245
  function repositoryBindingFromTask(task: TaskItem): RepositoryBinding | undefined {
210
246
  return task.repositoryBinding ? publicRepositoryBinding(task.repositoryBinding as RepositoryBinding) : undefined;
211
247
  }
@@ -856,7 +892,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
856
892
  }
857
893
 
858
894
  const startedJobs: Array<{ jobId: string; taskId: string }> = [];
859
- const failedSchedules: string[] = [];
895
+ const failedSchedules: Array<{
896
+ task: TaskItem & { id: string };
897
+ taskIndex: number;
898
+ message: string;
899
+ signalSkip: boolean;
900
+ duplicateDisposition?: DuplicateDisposition;
901
+ }> = [];
860
902
  let completedJobs = 0;
861
903
  let failedJobs = 0;
862
904
 
@@ -883,21 +925,106 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
883
925
  };
884
926
 
885
927
  const maxConcurrency = this.session.settings.get("task.maxConcurrency");
928
+ const ownerId = this.session.getAgentId?.() ?? undefined;
929
+ const parentSession = this.session.getSessionFile();
930
+ const admitDuplicateLaunch = (
931
+ subagentId: string,
932
+ task: TaskItem,
933
+ policy: "warn" | "supersede",
934
+ selfSubagentId?: string,
935
+ ): { ok: boolean; disposition?: DuplicateDisposition; error?: string; identity: DuplicateIdentity } => {
936
+ const identity = duplicateIdentityForTask(task, params.agent, ownerId, parentSession);
937
+ const key = duplicateIdentityKey(identity);
938
+ const predecessors = manager
939
+ .getSubagentRecords({ ownerId })
940
+ .filter(record => {
941
+ if (record.subagentId === selfSubagentId || record.subagentId === subagentId) return false;
942
+ if (record.status !== "running" && record.status !== "paused" && record.status !== "queued")
943
+ return false;
944
+ return record.duplicateIdentity === key;
945
+ })
946
+ .sort(
947
+ (a, b) =>
948
+ (b.currentJobId ? (manager.getJob(b.currentJobId)?.startTime ?? 0) : 0) -
949
+ (a.currentJobId ? (manager.getJob(a.currentJobId)?.startTime ?? 0) : 0) ||
950
+ a.subagentId.localeCompare(b.subagentId),
951
+ );
952
+ if (predecessors.length === 0) return { ok: true, identity };
953
+ if (policy === "warn")
954
+ return {
955
+ ok: true,
956
+ identity,
957
+ disposition: { action: "warned", predecessorIds: predecessors.map(record => record.subagentId) },
958
+ };
959
+ for (const predecessor of predecessors)
960
+ if (!manager.cancelSubagent(predecessor.subagentId, { ownerId }))
961
+ return { ok: false, identity, error: "duplicate_supersede_failed" };
962
+ return {
963
+ ok: true,
964
+ identity,
965
+ disposition: { action: "superseded", predecessorIds: predecessors.map(record => record.subagentId) },
966
+ };
967
+ };
886
968
  let resumeRunner: ResumeRunner | undefined;
887
969
  if (typeof manager.setResumeRunner === "function") {
888
970
  resumeRunner = (_subagentId, message, resumeDescriptor) => {
889
971
  const descriptor = isTaskResumeDescriptor(resumeDescriptor?.data) ? resumeDescriptor.data : undefined;
890
972
  if (!descriptor) return undefined;
973
+ const admission = (() => {
974
+ const identity = descriptor.duplicateIdentity;
975
+ const key = duplicateIdentityKey(identity);
976
+ const predecessors = manager
977
+ .getSubagentRecords({ ownerId })
978
+ .filter(record => {
979
+ if (
980
+ record.subagentId === descriptor.task.id ||
981
+ !["running", "paused", "queued"].includes(record.status)
982
+ )
983
+ return false;
984
+ return record.duplicateIdentity === key;
985
+ })
986
+ .sort(
987
+ (a, b) =>
988
+ (b.currentJobId ? (manager.getJob(b.currentJobId)?.startTime ?? 0) : 0) -
989
+ (a.currentJobId ? (manager.getJob(a.currentJobId)?.startTime ?? 0) : 0) ||
990
+ a.subagentId.localeCompare(b.subagentId),
991
+ );
992
+ if (predecessors.length === 0) return { ok: true, identity };
993
+ if (descriptor.duplicatePolicy === "warn")
994
+ return {
995
+ ok: true,
996
+ identity,
997
+ disposition: { action: "warned", predecessorIds: predecessors.map(record => record.subagentId) },
998
+ };
999
+ for (const predecessor of predecessors)
1000
+ if (!manager.cancelSubagent(predecessor.subagentId, { ownerId }))
1001
+ return { ok: false, identity, error: "duplicate_supersede_failed" };
1002
+ return {
1003
+ ok: true,
1004
+ identity,
1005
+ disposition: { action: "superseded", predecessorIds: predecessors.map(record => record.subagentId) },
1006
+ };
1007
+ })();
1008
+ if (!admission.ok) {
1009
+ descriptor.lastAdmissionFailure = admission.error;
1010
+ return undefined;
1011
+ }
1012
+ if (!descriptor) return undefined;
891
1013
  const forkSeeds = descriptor.forkContextSeed
892
1014
  ? new Map([[descriptor.task.id, descriptor.forkContextSeed]])
893
1015
  : undefined;
1016
+ const resumedTask = {
1017
+ ...descriptor.task,
1018
+ repositoryBinding: descriptor.repositoryBinding,
1019
+ duplicate_policy: descriptor.duplicatePolicy,
1020
+ };
894
1021
  return manager.register(
895
1022
  "task",
896
1023
  descriptor.task.id,
897
1024
  async ({ signal: runSignal }) => {
898
1025
  const result = await this.#executeSync(
899
1026
  descriptor.toolCallId,
900
- { ...descriptor.params, tasks: [descriptor.task] },
1027
+ { ...descriptor.params, tasks: [resumedTask] },
901
1028
  runSignal,
902
1029
  undefined,
903
1030
  [descriptor.task.id],
@@ -918,7 +1045,22 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
918
1045
  },
919
1046
  );
920
1047
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
921
- const singleResult = result.details?.results[0];
1048
+ const rawSingleResult = result.details?.results[0];
1049
+ const singleResult = rawSingleResult
1050
+ ? {
1051
+ ...rawSingleResult,
1052
+ duplicateDisposition: (() => {
1053
+ const initial = descriptor.initialDisposition;
1054
+ const current = admission.disposition;
1055
+ if (!initial) return current;
1056
+ if (!current) return initial;
1057
+ return {
1058
+ action: current.action,
1059
+ predecessorIds: [...new Set([...initial.predecessorIds, ...current.predecessorIds])],
1060
+ };
1061
+ })(),
1062
+ }
1063
+ : rawSingleResult;
922
1064
  return subagentRunOutcomeFromSingleResult(finalText, singleResult);
923
1065
  },
924
1066
  {
@@ -929,6 +1071,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
929
1071
  id: descriptor.task.id,
930
1072
  agent: descriptor.params.agent,
931
1073
  agentSource: descriptor.agentSource,
1074
+ duplicateIdentity: duplicateIdentityKey(descriptor.duplicateIdentity),
1075
+ duplicateDisposition: (admission.disposition?.action ??
1076
+ descriptor.initialDisposition?.action) as DuplicateDisposition["action"] | undefined,
932
1077
  description: descriptor.task.description,
933
1078
  assignment: descriptor.task.assignment.trim(),
934
1079
  },
@@ -965,7 +1110,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
965
1110
  for (let i = 0; i < taskItems.length; i++) {
966
1111
  const taskItem = taskItems[i];
967
1112
  if (signal?.aborted) {
968
- failedSchedules.push(`${taskItem.id}: cancelled before scheduling`);
1113
+ failedSchedules.push({
1114
+ task: taskItem as TaskItem & { id: string },
1115
+ taskIndex: i,
1116
+ message: "cancelled before scheduling",
1117
+ signalSkip: true,
1118
+ });
969
1119
  const progress = progressByTaskId.get(taskItem.id);
970
1120
  if (progress) {
971
1121
  progress.status = "aborted";
@@ -978,7 +1128,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
978
1128
  if (signal?.aborted) {
979
1129
  for (let skippedIndex = i; skippedIndex < taskItems.length; skippedIndex++) {
980
1130
  const skippedTask = taskItems[skippedIndex]!;
981
- failedSchedules.push(`${skippedTask.id}: cancelled before scheduling`);
1131
+ failedSchedules.push({
1132
+ task: skippedTask as TaskItem & { id: string },
1133
+ taskIndex: skippedIndex,
1134
+ message: "cancelled before scheduling",
1135
+ signalSkip: true,
1136
+ });
982
1137
  const skippedProgress = progressByTaskId.get(skippedTask.id);
983
1138
  if (skippedProgress) skippedProgress.status = "aborted";
984
1139
  }
@@ -994,6 +1149,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
994
1149
  const subtaskSessionFile = managedPersistence
995
1150
  ? null
996
1151
  : path.join(batchArtifactsDir ?? externalTaskSessionsDir!, `${uniqueId}.jsonl`);
1152
+ const admission = admitDuplicateLaunch(uniqueId, taskItem, taskItem.duplicate_policy ?? "warn");
1153
+ if (!admission.ok) {
1154
+ failedSchedules.push({
1155
+ task: taskItem as TaskItem & { id: string },
1156
+ taskIndex: i,
1157
+ message: admission.error ?? "duplicate_supersede_failed",
1158
+ signalSkip: false,
1159
+ duplicateDisposition: admission.disposition,
1160
+ });
1161
+ continue;
1162
+ }
997
1163
  const jobId = manager.register(
998
1164
  "task",
999
1165
  label,
@@ -1033,7 +1199,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1033
1199
  },
1034
1200
  );
1035
1201
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
1036
- const singleResult = result.details?.results[0];
1202
+ const rawSingleResult = result.details?.results[0];
1203
+ const singleResult = rawSingleResult
1204
+ ? {
1205
+ ...rawSingleResult,
1206
+ duplicateDisposition: rawSingleResult.duplicateDisposition ?? admission.disposition,
1207
+ }
1208
+ : rawSingleResult;
1037
1209
  if (progress) {
1038
1210
  progress.status = singleResult?.paused
1039
1211
  ? "paused"
@@ -1116,6 +1288,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1116
1288
  id: uniqueId,
1117
1289
  agent: params.agent,
1118
1290
  agentSource: fallbackAgentSource,
1291
+ duplicateIdentity: duplicateIdentityKey(admission.identity),
1292
+ duplicateDisposition: admission.disposition?.action,
1119
1293
  description: taskItem.description,
1120
1294
  assignment: taskItem.assignment.trim(),
1121
1295
  },
@@ -1142,6 +1316,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1142
1316
  durableOutputAllowed: Boolean(batchArtifactsDir),
1143
1317
  forkContextSeed: frozenForkSeed,
1144
1318
  agentSource: fallbackAgentSource,
1319
+ repositoryBinding: taskItem.repositoryBinding as RepositoryBinding,
1320
+ duplicateIdentity: admission.identity,
1321
+ duplicatePolicy: taskItem.duplicate_policy ?? "warn",
1322
+ initialDisposition: admission.disposition,
1145
1323
  } satisfies TaskResumeDescriptor,
1146
1324
  },
1147
1325
  resumeRunner,
@@ -1152,10 +1330,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1152
1330
  subagentId: uniqueId,
1153
1331
  ownerId: this.session.getAgentId?.() ?? undefined,
1154
1332
  currentJobId: jobId,
1333
+ currentJobGeneration: manager.getJob(jobId)?.generation,
1155
1334
  historicalJobIds: [],
1156
1335
  status: manager.getJob(jobId)?.status ?? "running",
1157
1336
  sessionFile: null,
1158
1337
  resumable: true,
1338
+ duplicateIdentity: duplicateIdentityKey(admission.identity),
1339
+ duplicateDisposition: admission.disposition?.action,
1159
1340
  });
1160
1341
  }
1161
1342
  } catch (error) {
@@ -1165,7 +1346,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1165
1346
  : error instanceof Error
1166
1347
  ? error.message
1167
1348
  : String(error);
1168
- failedSchedules.push(`${taskItem.id}: ${message}`);
1349
+ failedSchedules.push({
1350
+ task: taskItem as TaskItem & { id: string },
1351
+ taskIndex: i,
1352
+ message,
1353
+ signalSkip: false,
1354
+ });
1169
1355
  const progress = progressByTaskId.get(taskItem.id);
1170
1356
  if (progress) {
1171
1357
  progress.status = "failed";
@@ -1178,11 +1364,36 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1178
1364
  failedJobs += failedSchedules.length;
1179
1365
  }
1180
1366
 
1367
+ const scheduleFailureReceipts = failedSchedules
1368
+ .slice()
1369
+ .sort((a, b) => a.taskIndex - b.taskIndex)
1370
+ .map((entry, index) =>
1371
+ buildTaskReceipt({
1372
+ index,
1373
+ id: entry.task.id,
1374
+ agent: params.agent,
1375
+ agentSource: fallbackAgentSource,
1376
+ task: renderTaskAssignment(entry.task.assignment, simpleMode),
1377
+ assignment: entry.task.assignment,
1378
+ description: entry.task.description,
1379
+ status: "failed",
1380
+ exitCode: 1,
1381
+ aborted: entry.signalSkip ? true : undefined,
1382
+ abortReason: entry.signalSkip ? "Cancelled before start" : undefined,
1383
+ truncated: false,
1384
+ durationMs: 0,
1385
+ tokens: 0,
1386
+ output: "",
1387
+ stderr: entry.message,
1388
+ error: entry.message,
1389
+ duplicateDisposition: entry.duplicateDisposition,
1390
+ } as SingleResult),
1391
+ );
1181
1392
  if (startedJobs.length === 0) {
1182
- const failureText = `Failed to start background task jobs: ${failedSchedules.join("; ")}`;
1393
+ const failureText = `Failed to start background task jobs: ${failedSchedules.map(entry => `${entry.task.id}: ${entry.message}`).join("; ")}`;
1183
1394
  return {
1184
1395
  content: [{ type: "text", text: failureText }],
1185
- details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
1396
+ details: { projectAgentsDir: null, results: scheduleFailureReceipts, totalDurationMs: 0 },
1186
1397
  };
1187
1398
  }
1188
1399
 
@@ -1230,7 +1441,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1230
1441
  ],
1231
1442
  details: {
1232
1443
  projectAgentsDir: null,
1233
- results: [],
1444
+ results: scheduleFailureReceipts,
1234
1445
  totalDurationMs: 0,
1235
1446
  progress: getProgressSnapshot(),
1236
1447
  async: { state: asyncState, jobId: startedJobs[0].jobId, type: "task" },
@@ -3,6 +3,7 @@ import type { AgentProgress, TaskToolDetails } from "./types";
3
3
  export type ProviderRetryKind = NonNullable<AgentProgress["retryState"]>["kind"];
4
4
 
5
5
  const FIRST_EVENT_TIMEOUT_PATTERN = /stream timed out while waiting for the first event/i;
6
+ const FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_MESSAGE = "Provider stream timed out while waiting for first event";
6
7
  const IDLE_STREAM_STALL_PATTERN = /stream stalled while waiting for the next event/i;
7
8
  const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
8
9
 
@@ -22,7 +23,9 @@ export function classifyProviderRetryFromTransport(facts: {
22
23
  return "first_event_timeout";
23
24
  }
24
25
  const errorMessage = facts.errorMessage ?? "";
25
- if (FIRST_EVENT_TIMEOUT_PATTERN.test(errorMessage)) return "first_event_timeout";
26
+ if (FIRST_EVENT_TIMEOUT_PATTERN.test(errorMessage) || errorMessage === FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_MESSAGE) {
27
+ return "first_event_timeout";
28
+ }
26
29
  if (IDLE_STREAM_STALL_PATTERN.test(errorMessage)) return "idle_stream_stall";
27
30
  return "provider_error";
28
31
  }
@@ -43,6 +43,7 @@ export interface TaskResultReceipt {
43
43
  retryFailure?: { attempt: number; errorSummary: string };
44
44
  setupFailure?: { summary: string };
45
45
  errorSummary?: string;
46
+ duplicateDisposition?: SingleResult["duplicateDisposition"];
46
47
  abortSummary?: string;
47
48
  preview: string;
48
49
  previewTruncated: boolean;
@@ -293,6 +294,7 @@ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
293
294
  retryFailure: raw.retryFailure
294
295
  ? { attempt: raw.retryFailure.attempt, errorSummary: "Retry failure recorded." }
295
296
  : undefined,
297
+ duplicateDisposition: raw.duplicateDisposition,
296
298
  errorSummary:
297
299
  raw.setupFailure?.summary ??
298
300
  (raw.error
package/src/task/types.ts CHANGED
@@ -114,6 +114,7 @@ const createTaskItemSchema = (_contextEnabled: boolean) =>
114
114
  .describe(
115
115
  "authoritative repository identity; omitted items are stamped from session cwd before discovery/spawn and still fail closed on sibling drift",
116
116
  ),
117
+ duplicate_policy: z.enum(["warn", "supersede"]).optional().describe("duplicate launch policy; defaults to warn"),
117
118
  });
118
119
 
119
120
  /** Single task item for parallel execution (default shape with context enabled). */
@@ -199,6 +200,7 @@ export interface TaskParams {
199
200
  spawnPlan?: SpawnPlanReceipt;
200
201
  tasks: TaskItem[];
201
202
  isolated?: boolean;
203
+ duplicate_policy?: "warn" | "supersede";
202
204
  }
203
205
 
204
206
  /** A code review finding reported by the reviewer agent */
@@ -433,6 +435,12 @@ export interface TaskRecoveryArtifactRef {
433
435
  durability: "session";
434
436
  }
435
437
 
438
+ /** Bounded duplicate-launch disposition carried into task receipts. */
439
+ export interface DuplicateDisposition {
440
+ action: "warned" | "superseded";
441
+ predecessorIds: string[];
442
+ }
443
+
436
444
  export interface TaskPersistenceResult {
437
445
  outcome: "applied" | "no_changes" | "recovery_available";
438
446
  ownerWorktreeApplied: boolean;
@@ -469,6 +477,8 @@ export interface SingleResult {
469
477
  setupFailure?: SetupFailureSummary;
470
478
  aborted?: boolean;
471
479
  abortReason?: string;
480
+
481
+ duplicateDisposition?: DuplicateDisposition;
472
482
  paused?: boolean;
473
483
  /** Aggregated usage from the subprocess, accumulated incrementally from message_end events. */
474
484
  usage?: Usage;
@@ -51,6 +51,7 @@ const MONITOR_LABEL_MAX = 120;
51
51
  const MAX_PENDING_MONITOR_NOTIFICATIONS = 3;
52
52
  const MONITOR_NOTIFICATION_LINE_MAX_BYTES = 16 * 1024;
53
53
  const MONITOR_NOTIFICATION_LINE_MAX_LINES = 20;
54
+ const PERSISTENT_MONITOR_DEBOUNCE_MS = 250;
54
55
 
55
56
  function buildMonitorLabel(params: MonitorParams): string {
56
57
  const base = `[monitor:${params.kind}] ${params.description}`;
@@ -124,6 +125,7 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
124
125
  const controller = { closed: false };
125
126
  let currentJobId = "";
126
127
  let sequence = 0;
128
+ let flushTimer: NodeJS.Timeout | undefined;
127
129
  let latestLine: string | undefined;
128
130
  let coalescedCount = 0;
129
131
  let flushScheduled = false;
@@ -136,6 +138,10 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
136
138
  (message.details as { taskId?: string } | undefined)?.taskId === currentJobId;
137
139
  const flushLatest = () => {
138
140
  if (!persistent || latestLine === undefined) return;
141
+ if (flushTimer) {
142
+ clearTimeout(flushTimer);
143
+ flushTimer = undefined;
144
+ }
139
145
  const line = latestLine;
140
146
  const count = coalescedCount;
141
147
  latestLine = undefined;
@@ -205,7 +211,7 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
205
211
  coalescedCount += flushScheduled ? 1 : 0;
206
212
  if (flushScheduled) return;
207
213
  flushScheduled = true;
208
- queueMicrotask(flushLatest);
214
+ flushTimer = setTimeout(flushLatest, PERSISTENT_MONITOR_DEBOUNCE_MS);
209
215
  };
210
216
  const monitorJob = await bash.startMonitorJob(
211
217
  { command: params.command, timeout: params.timeout },
@@ -818,7 +818,12 @@ export const BASH_DEFAULT_OUTPUT_TAIL_BYTES = 1024;
818
818
  * budget still wins.
819
819
  */
820
820
  export function resolveBashOutputSinkTailBytes(s: Settings): number {
821
- return s.has("tools.artifactTailBytes") ? s.get("tools.artifactTailBytes") * 1024 : BASH_DEFAULT_OUTPUT_TAIL_BYTES;
821
+ const configuredTailBytes = s.get("tools.artifactTailBytes");
822
+ const hasExplicitTailBytes =
823
+ typeof s.has === "function" ? s.has("tools.artifactTailBytes") : configuredTailBytes !== undefined;
824
+ return hasExplicitTailBytes && configuredTailBytes !== undefined
825
+ ? configuredTailBytes * 1024
826
+ : BASH_DEFAULT_OUTPUT_TAIL_BYTES;
822
827
  }
823
828
  /**
824
829
  * Bash keeps only the tail unless the user explicitly opts into the shared
@@ -826,7 +831,10 @@ export function resolveBashOutputSinkTailBytes(s: Settings): number {
826
831
  * tools without silently turning Bash back into middle-elision mode.
827
832
  */
828
833
  export function resolveBashOutputSinkHeadBytes(s: Settings): number {
829
- return s.has("tools.artifactHeadBytes") ? resolveOutputSinkHeadBytes(s) : 0;
834
+ const configuredHeadBytes = s.get("tools.artifactHeadBytes");
835
+ const hasExplicitHeadBytes =
836
+ typeof s.has === "function" ? s.has("tools.artifactHeadBytes") : configuredHeadBytes !== undefined;
837
+ return hasExplicitHeadBytes && configuredHeadBytes !== undefined ? configuredHeadBytes * 1024 : 0;
830
838
  }
831
839
 
832
840
  /**