@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.
- package/CHANGELOG.md +34 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +43 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +430 -46
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/modes/acp/acp-agent.ts +27 -13
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +691 -92
- package/src/sdk/bus/chat-daemon-control.ts +30 -6
- package/src/sdk/bus/index.ts +23 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/session.ts +7 -6
- package/src/session/agent-session.ts +752 -339
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/internal/managed-session-scope.ts +252 -67
- package/src/session/internal/managed-session-storage.ts +1 -0
- package/src/session/session-manager.ts +7 -0
- package/src/session/session-storage.ts +146 -24
- package/src/task/index.ts +230 -19
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +2 -0
- package/src/task/types.ts +10 -0
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/subagent.ts +97 -75
- 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
|
-
|
|
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:
|
|
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: [
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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:
|
|
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))
|
|
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
|
}
|
package/src/task/receipt.ts
CHANGED
|
@@ -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;
|
package/src/tools/monitor.ts
CHANGED
|
@@ -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
|
-
|
|
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 },
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
/**
|