@teamclaws/teamclaw 2026.3.25 → 2026.3.26-2

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/ui/style.css CHANGED
@@ -301,6 +301,19 @@ body {
301
301
  margin-bottom: 6px;
302
302
  line-height: 1.5;
303
303
  }
304
+ .task-contract-summary {
305
+ margin-bottom: 8px;
306
+ padding: 8px 10px;
307
+ border-radius: 10px;
308
+ border: 1px solid rgba(99, 102, 241, 0.2);
309
+ background: rgba(99, 102, 241, 0.08);
310
+ color: var(--text-secondary);
311
+ font-size: 12px;
312
+ line-height: 1.5;
313
+ }
314
+ .task-contract-summary strong {
315
+ color: var(--text-primary);
316
+ }
304
317
  .skill-pills {
305
318
  display: flex;
306
319
  flex-wrap: wrap;
@@ -558,6 +571,135 @@ body {
558
571
  font-size: 13px;
559
572
  }
560
573
 
574
+ .contract-card {
575
+ background: var(--bg-tertiary);
576
+ border: 1px solid var(--border);
577
+ border-left: 3px solid var(--accent);
578
+ border-radius: 12px;
579
+ padding: 12px 14px;
580
+ display: flex;
581
+ flex-direction: column;
582
+ gap: 12px;
583
+ }
584
+
585
+ .contract-card-completed { border-left-color: var(--success); }
586
+ .contract-card-failed { border-left-color: var(--danger); }
587
+ .contract-card-blocked,
588
+ .contract-card-handoff { border-left-color: var(--warning); }
589
+ .contract-card-in_progress,
590
+ .contract-card-review,
591
+ .contract-card-update,
592
+ .contract-card-coordination,
593
+ .contract-card-question { border-left-color: var(--info); }
594
+ .contract-card-review-request,
595
+ .contract-card-review-response { border-left-color: #8b5cf6; }
596
+ .contract-card-announcement,
597
+ .contract-card-manifest { border-left-color: var(--accent-hover); }
598
+
599
+ .contract-card-header {
600
+ display: flex;
601
+ flex-direction: column;
602
+ gap: 10px;
603
+ }
604
+
605
+ .contract-card-kicker,
606
+ .contract-meta-label,
607
+ .contract-section-title,
608
+ .message-meta {
609
+ color: var(--text-muted);
610
+ font-size: 11px;
611
+ font-weight: 700;
612
+ letter-spacing: 0.06em;
613
+ text-transform: uppercase;
614
+ }
615
+
616
+ .contract-card-title {
617
+ font-size: 15px;
618
+ line-height: 1.45;
619
+ color: var(--text-primary);
620
+ }
621
+
622
+ .contract-meta-grid {
623
+ display: grid;
624
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
625
+ gap: 10px;
626
+ }
627
+
628
+ .contract-meta-item {
629
+ padding: 10px 12px;
630
+ border: 1px solid rgba(255, 255, 255, 0.06);
631
+ border-radius: 10px;
632
+ background: rgba(255, 255, 255, 0.02);
633
+ }
634
+
635
+ .contract-meta-value {
636
+ margin-top: 4px;
637
+ font-size: 13px;
638
+ color: var(--text-primary);
639
+ line-height: 1.5;
640
+ }
641
+
642
+ .contract-section {
643
+ display: flex;
644
+ flex-direction: column;
645
+ gap: 8px;
646
+ }
647
+
648
+ .contract-markdown,
649
+ .timeline-raw-markdown,
650
+ .timeline-contract-note {
651
+ font-size: 13px;
652
+ line-height: 1.6;
653
+ color: var(--text-primary);
654
+ }
655
+
656
+ .contract-list {
657
+ padding-left: 18px;
658
+ color: var(--text-primary);
659
+ display: flex;
660
+ flex-direction: column;
661
+ gap: 6px;
662
+ }
663
+
664
+ .contract-list li {
665
+ line-height: 1.6;
666
+ }
667
+
668
+ .contract-chip-row {
669
+ display: flex;
670
+ flex-wrap: wrap;
671
+ gap: 8px;
672
+ }
673
+
674
+ .contract-chip {
675
+ display: inline-flex;
676
+ align-items: center;
677
+ min-height: 28px;
678
+ padding: 4px 10px;
679
+ border-radius: 999px;
680
+ border: 1px solid rgba(96, 165, 250, 0.22);
681
+ background: rgba(96, 165, 250, 0.1);
682
+ color: #c7d2fe;
683
+ font-size: 12px;
684
+ font-weight: 600;
685
+ }
686
+
687
+ .contract-reference-row .contract-chip {
688
+ border-color: rgba(52, 211, 153, 0.22);
689
+ background: rgba(52, 211, 153, 0.08);
690
+ color: #bbf7d0;
691
+ }
692
+
693
+ .contract-role-row .contract-chip {
694
+ border-color: rgba(168, 85, 247, 0.22);
695
+ background: rgba(168, 85, 247, 0.08);
696
+ color: #e9d5ff;
697
+ }
698
+
699
+ .contract-inline-note {
700
+ color: var(--text-secondary);
701
+ }
702
+
561
703
  .messages-feed {
562
704
  display: flex;
563
705
  flex-direction: column;
@@ -592,6 +734,12 @@ body {
592
734
  .message-type.review-request { background: #7c3aed; color: #fff; }
593
735
 
594
736
  .message-content { font-size: 13px; color: var(--text-primary); line-height: 1.5; }
737
+ .message-meta {
738
+ margin-bottom: 8px;
739
+ }
740
+ .message-card .contract-card {
741
+ margin-bottom: 10px;
742
+ }
595
743
 
596
744
  .create-task-form {
597
745
  max-width: 500px;
@@ -1327,6 +1475,10 @@ body {
1327
1475
  word-break: break-word;
1328
1476
  overflow-wrap: anywhere;
1329
1477
  }
1478
+ .timeline-entry-body .contract-card + .timeline-raw-markdown,
1479
+ .timeline-entry-body .timeline-contract-note + .timeline-contract-note {
1480
+ margin-top: 10px;
1481
+ }
1330
1482
 
1331
1483
  .task-detail-output-stream {
1332
1484
  display: flex;
@@ -62,6 +62,12 @@ export function createWorkerHttpHandler(
62
62
  const recommendedSkills = Array.isArray(body.recommendedSkills)
63
63
  ? body.recommendedSkills.map((entry) => String(entry ?? ""))
64
64
  : undefined;
65
+ const executionSessionKey = typeof body.executionSessionKey === "string"
66
+ ? body.executionSessionKey
67
+ : undefined;
68
+ const executionIdempotencyKey = typeof body.executionIdempotencyKey === "string"
69
+ ? body.executionIdempotencyKey
70
+ : undefined;
65
71
  const repo = body.repo && typeof body.repo === "object"
66
72
  ? body.repo as TaskAssignmentPayload["repo"]
67
73
  : undefined;
@@ -74,13 +80,15 @@ export function createWorkerHttpHandler(
74
80
  logger.info(`Worker: received task assignment - ${title} (${taskId})`);
75
81
 
76
82
  if (taskExecutor && resultReporter) {
77
- taskExecutor({
78
- taskId,
79
- title,
80
- description,
81
- recommendedSkills,
82
- repo,
83
- })
83
+ taskExecutor({
84
+ taskId,
85
+ title,
86
+ description,
87
+ recommendedSkills,
88
+ executionSessionKey,
89
+ executionIdempotencyKey,
90
+ repo,
91
+ })
84
92
  .then((result) => {
85
93
  if (isTaskCancelled?.(taskId)) {
86
94
  logger.info(`Worker: skipping result report for cancelled task ${taskId}`);
@@ -51,6 +51,8 @@ export function createWorkerPromptInjector(
51
51
  parts.push("11. Treat file paths from documents, plans, and teammate messages as hints, not guarantees. Verify the real path exists in the current workspace before reading or editing it; if it does not exist, search for the closest real file and note the drift instead of repeatedly calling missing paths.");
52
52
  parts.push("12. The workspace may be backed by a TeamClaw-managed git repository. Treat the current checkout as canonical project state; do not delete `.git` or replace the repo with ad-hoc archives.");
53
53
  parts.push("13. If the assigned task includes recommended skills, use those exact skill slugs first. Missing skills should be searched/installed before execution when supported by the runtime.");
54
+ parts.push("14. Important: submit structured collaboration contracts, not only prose. Use teamclaw_submit_result_contract before your final reply, use structured fields on progress/handoff/review/message tools, and use clarification tools instead of hiding questions inside freeform output.");
55
+ parts.push("15. Do not use sessions_yield or end your turn while background work, coding agents, or process sessions are still running. A TeamClaw task is only done when you have the real final deliverable, not when a helper session is still working.");
54
56
  parts.push(`Worker ID: ${identity.workerId}`);
55
57
  parts.push(`Controller: ${identity.controllerUrl}`);
56
58
 
@@ -7,6 +7,7 @@ import { resolveDefaultOpenClawWorkspaceDir } from "../openclaw-workspace.js";
7
7
  import type { TaskAssignmentPayload, TaskExecutionEventInput } from "../types.js";
8
8
 
9
9
  type SkillCli = "openclaw" | "clawhub";
10
+ const ON_DEMAND_DISCOVERY_SKILLS = new Set(["find-skills"]);
10
11
 
11
12
  type CommandResult = {
12
13
  ok: boolean;
@@ -185,6 +186,18 @@ export async function installRecommendedSkills(
185
186
  });
186
187
 
187
188
  for (const requestedSkill of recommendedSkills) {
189
+ if (ON_DEMAND_DISCOVERY_SKILLS.has(normalizeKey(requestedSkill))) {
190
+ skipped.push(requestedSkill);
191
+ events.push({
192
+ type: "lifecycle",
193
+ phase: "skill_install_skipped",
194
+ source: "worker",
195
+ status: "running",
196
+ message: `Skipping automatic install of discovery skill ${requestedSkill}; invoke it on demand inside the task if needed.`,
197
+ });
198
+ continue;
199
+ }
200
+
188
201
  let resolvedSlug = isSkillSlug(requestedSkill) ? requestedSkill : undefined;
189
202
  const installedPath = resolvedSlug ? buildInstalledSkillPath(workspaceDir, resolvedSlug) : "";
190
203
 
@@ -1,4 +1,15 @@
1
1
  import { Type } from "@sinclair/typebox";
2
+ import {
3
+ backfillWorkerProgressContract,
4
+ ensureTeamMessageContract,
5
+ normalizeContractRole,
6
+ normalizeContractStringList,
7
+ normalizeOptionalContractText,
8
+ normalizeTaskHandoffContract,
9
+ normalizeWorkerProgressContract,
10
+ normalizeWorkerTaskResultContract,
11
+ renderWorkerProgressText,
12
+ } from "../interaction-contracts.js";
2
13
  import type { PluginConfig, WorkerIdentity } from "../types.js";
3
14
 
4
15
  const ALLOWED_PROGRESS_STATUSES = new Set(["in_progress", "review"]);
@@ -20,6 +31,10 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
20
31
  targetRole: Type.String({ description: "Exact target role ID (pm, architect, developer, qa, release-engineer, infra-engineer, devops, security-engineer, designer, marketing)" }),
21
32
  question: Type.String({ description: "The question to ask" }),
22
33
  taskId: Type.Optional(Type.String({ description: "Related task ID if any" })),
34
+ summary: Type.Optional(Type.String({ description: "Short structured summary for this question" })),
35
+ details: Type.Optional(Type.String({ description: "Optional extra context for the peer" })),
36
+ requestedAction: Type.Optional(Type.String({ description: "Concrete response/action needed from the peer" })),
37
+ references: Type.Optional(Type.Array(Type.String({ description: "Relevant task IDs, file paths, or artifact references" }))),
23
38
  }),
24
39
  async execute(_id: string, params: Record<string, unknown>) {
25
40
  const identity = getIdentity();
@@ -29,12 +44,25 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
29
44
 
30
45
  const targetRole = String(params.targetRole ?? "");
31
46
  const question = String(params.question ?? "");
47
+ const normalizedTargetRole = normalizeContractRole(targetRole);
32
48
 
33
49
  if (!targetRole || !question) {
34
50
  return { content: [{ type: "text" as const, text: "targetRole and question are required." }] };
35
51
  }
36
52
 
37
53
  try {
54
+ const contract = ensureTeamMessageContract(null, {
55
+ type: "direct",
56
+ content: question,
57
+ toRole: normalizedTargetRole,
58
+ taskId: typeof params.taskId === "string" ? params.taskId : undefined,
59
+ summary: typeof params.summary === "string" ? params.summary : undefined,
60
+ details: typeof params.details === "string" ? params.details : undefined,
61
+ requestedAction: typeof params.requestedAction === "string" ? params.requestedAction : undefined,
62
+ references: normalizeContractStringList(params.references),
63
+ intent: "question",
64
+ needsResponse: true,
65
+ });
38
66
  const res = await fetch(`${identity.controllerUrl}/api/v1/messages/direct`, {
39
67
  method: "POST",
40
68
  headers: { "Content-Type": "application/json" },
@@ -44,6 +72,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
44
72
  toRole: targetRole,
45
73
  content: question,
46
74
  taskId: params.taskId ?? null,
75
+ contract,
47
76
  }),
48
77
  });
49
78
 
@@ -64,6 +93,11 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
64
93
  parameters: Type.Object({
65
94
  message: Type.String({ description: "The message to broadcast" }),
66
95
  taskId: Type.Optional(Type.String({ description: "Related task ID if any" })),
96
+ summary: Type.Optional(Type.String({ description: "Short structured summary for the broadcast" })),
97
+ details: Type.Optional(Type.String({ description: "Optional extra context for the team" })),
98
+ requestedAction: Type.Optional(Type.String({ description: "Optional action the team should take after reading this message" })),
99
+ needsResponse: Type.Optional(Type.Boolean({ description: "Whether the broadcast expects a response from recipients" })),
100
+ references: Type.Optional(Type.Array(Type.String({ description: "Relevant task IDs, file paths, or artifact references" }))),
67
101
  }),
68
102
  async execute(_id: string, params: Record<string, unknown>) {
69
103
  const identity = getIdentity();
@@ -77,6 +111,17 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
77
111
  }
78
112
 
79
113
  try {
114
+ const contract = ensureTeamMessageContract(null, {
115
+ type: "broadcast",
116
+ content: message,
117
+ taskId: typeof params.taskId === "string" ? params.taskId : undefined,
118
+ summary: typeof params.summary === "string" ? params.summary : undefined,
119
+ details: typeof params.details === "string" ? params.details : undefined,
120
+ requestedAction: typeof params.requestedAction === "string" ? params.requestedAction : undefined,
121
+ needsResponse: typeof params.needsResponse === "boolean" ? params.needsResponse : undefined,
122
+ references: normalizeContractStringList(params.references),
123
+ intent: "announcement",
124
+ });
80
125
  const res = await fetch(`${identity.controllerUrl}/api/v1/messages/broadcast`, {
81
126
  method: "POST",
82
127
  headers: { "Content-Type": "application/json" },
@@ -85,6 +130,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
85
130
  fromRole: identity.role,
86
131
  content: message,
87
132
  taskId: params.taskId ?? null,
133
+ contract,
88
134
  }),
89
135
  });
90
136
 
@@ -106,6 +152,9 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
106
152
  targetRole: Type.String({ description: "Exact target role ID to request review from" }),
107
153
  reviewContent: Type.String({ description: "Content to review or description of what needs review" }),
108
154
  taskId: Type.String({ description: "Related task ID" }),
155
+ summary: Type.Optional(Type.String({ description: "Short structured summary for the review request" })),
156
+ requestedAction: Type.Optional(Type.String({ description: "Concrete review action expected from the target role" })),
157
+ references: Type.Optional(Type.Array(Type.String({ description: "Relevant file paths, artifacts, or checks to review" }))),
109
158
  }),
110
159
  async execute(_id: string, params: Record<string, unknown>) {
111
160
  const identity = getIdentity();
@@ -122,6 +171,17 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
122
171
  }
123
172
 
124
173
  try {
174
+ const contract = ensureTeamMessageContract(null, {
175
+ type: "review-request",
176
+ content: reviewContent,
177
+ toRole: normalizeContractRole(targetRole),
178
+ taskId,
179
+ summary: typeof params.summary === "string" ? params.summary : undefined,
180
+ requestedAction: typeof params.requestedAction === "string" ? params.requestedAction : undefined,
181
+ references: normalizeContractStringList(params.references),
182
+ intent: "review-request",
183
+ needsResponse: true,
184
+ });
125
185
  const res = await fetch(`${identity.controllerUrl}/api/v1/messages/review-request`, {
126
186
  method: "POST",
127
187
  headers: { "Content-Type": "application/json" },
@@ -131,6 +191,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
131
191
  toRole: targetRole,
132
192
  content: reviewContent,
133
193
  taskId,
194
+ contract,
134
195
  }),
135
196
  });
136
197
 
@@ -152,6 +213,9 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
152
213
  taskId: Type.String({ description: "Task ID to hand off" }),
153
214
  targetRole: Type.String({ description: "Exact target role ID to hand off to" }),
154
215
  reason: Type.String({ description: "Reason for the handoff" }),
216
+ summary: Type.Optional(Type.String({ description: "Short structured summary for the handoff" })),
217
+ expectedNextStep: Type.Optional(Type.String({ description: "Concrete next step the receiving role should take" })),
218
+ artifacts: Type.Optional(Type.Array(Type.String({ description: "Files, task IDs, or artifacts the next role should inspect first" }))),
155
219
  }),
156
220
  async execute(_id: string, params: Record<string, unknown>) {
157
221
  const identity = getIdentity();
@@ -168,6 +232,13 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
168
232
  }
169
233
 
170
234
  try {
235
+ const contract = normalizeTaskHandoffContract(null, {
236
+ targetRole: normalizeContractRole(targetRole),
237
+ reason,
238
+ summary: typeof params.summary === "string" ? params.summary : undefined,
239
+ expectedNextStep: typeof params.expectedNextStep === "string" ? params.expectedNextStep : undefined,
240
+ artifacts: normalizeContractStringList(params.artifacts),
241
+ });
171
242
  const res = await fetch(`${identity.controllerUrl}/api/v1/tasks/${taskId}/handoff`, {
172
243
  method: "POST",
173
244
  headers: { "Content-Type": "application/json" },
@@ -175,6 +246,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
175
246
  fromWorkerId: identity.workerId,
176
247
  targetRole,
177
248
  reason,
249
+ contract,
178
250
  }),
179
251
  });
180
252
 
@@ -188,6 +260,83 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
188
260
  }
189
261
  },
190
262
  },
263
+ {
264
+ name: "teamclaw_submit_result_contract",
265
+ label: "Submit Result Contract",
266
+ description: "Record the structured completion/blocker contract for the current task before the final worker reply is returned",
267
+ parameters: Type.Object({
268
+ taskId: Type.String({ description: "Task ID" }),
269
+ outcome: Type.Optional(Type.String({ description: "Outcome: completed, blocked, or failed" })),
270
+ summary: Type.String({ description: "Short structured summary of the worker result" }),
271
+ deliverables: Type.Optional(
272
+ Type.Array(
273
+ Type.Object({
274
+ kind: Type.String({ description: "Deliverable kind: file, directory, command, artifact, or note" }),
275
+ value: Type.String({ description: "Deliverable identifier, path, or note" }),
276
+ summary: Type.Optional(Type.String({ description: "Optional short note about this deliverable" })),
277
+ }),
278
+ ),
279
+ ),
280
+ keyPoints: Type.Optional(Type.Array(Type.String({ description: "Important decisions, findings, or implementation notes" }))),
281
+ blockers: Type.Optional(Type.Array(Type.String({ description: "Any unresolved blockers or risks" }))),
282
+ followUps: Type.Optional(
283
+ Type.Array(
284
+ Type.Object({
285
+ type: Type.String({ description: "Follow-up type: review, handoff, clarification, downstream-task" }),
286
+ targetRole: Type.Optional(Type.String({ description: "Role that should handle the follow-up" })),
287
+ reason: Type.String({ description: "Why this follow-up is needed" }),
288
+ }),
289
+ ),
290
+ ),
291
+ questions: Type.Optional(Type.Array(Type.String({ description: "Any remaining open questions" }))),
292
+ notes: Type.Optional(Type.String({ description: "Optional extra delivery notes" })),
293
+ }),
294
+ async execute(_id: string, params: Record<string, unknown>) {
295
+ const identity = getIdentity();
296
+ if (!identity) {
297
+ return { content: [{ type: "text" as const, text: "Not registered with a team." }] };
298
+ }
299
+
300
+ const taskId = String(params.taskId ?? "");
301
+ if (!taskId) {
302
+ return { content: [{ type: "text" as const, text: "taskId is required." }] };
303
+ }
304
+
305
+ const contract = normalizeWorkerTaskResultContract({
306
+ version: "1.0",
307
+ outcome: typeof params.outcome === "string" ? params.outcome : "completed",
308
+ summary: params.summary,
309
+ deliverables: params.deliverables,
310
+ keyPoints: params.keyPoints,
311
+ blockers: params.blockers,
312
+ followUps: params.followUps,
313
+ questions: params.questions,
314
+ notes: params.notes,
315
+ });
316
+ if (!contract) {
317
+ return { content: [{ type: "text" as const, text: "summary is required for teamclaw_submit_result_contract." }] };
318
+ }
319
+
320
+ try {
321
+ const res = await fetch(`${identity.controllerUrl}/api/v1/tasks/${taskId}/result-contract`, {
322
+ method: "POST",
323
+ headers: { "Content-Type": "application/json" },
324
+ body: JSON.stringify({ contract, workerId: identity.workerId }),
325
+ });
326
+ if (!res.ok) {
327
+ return { content: [{ type: "text" as const, text: `Failed to submit result contract: ${res.status}` }] };
328
+ }
329
+ return {
330
+ content: [{
331
+ type: "text" as const,
332
+ text: `Result contract recorded for ${taskId}: ${contract.outcome} / ${contract.summary}`,
333
+ }],
334
+ };
335
+ } catch (err) {
336
+ return { content: [{ type: "text" as const, text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
337
+ }
338
+ },
339
+ },
191
340
  {
192
341
  name: "teamclaw_request_clarification",
193
342
  label: "Request Clarification",
@@ -267,8 +416,12 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
267
416
  description: "Report progress on an assigned task",
268
417
  parameters: Type.Object({
269
418
  taskId: Type.String({ description: "Task ID" }),
270
- progress: Type.String({ description: "Progress update message" }),
419
+ progress: Type.Optional(Type.String({ description: "Progress update message" })),
271
420
  status: Type.Optional(Type.String({ description: "Optional non-terminal status: in_progress or review. Do not use completed or failed here." })),
421
+ summary: Type.Optional(Type.String({ description: "Short structured progress summary" })),
422
+ currentStep: Type.Optional(Type.String({ description: "What the worker is doing right now" })),
423
+ nextStep: Type.Optional(Type.String({ description: "What the worker plans to do next" })),
424
+ blockers: Type.Optional(Type.Array(Type.String({ description: "Any blockers slowing progress" }))),
272
425
  }),
273
426
  async execute(_id: string, params: Record<string, unknown>) {
274
427
  const identity = getIdentity();
@@ -277,12 +430,15 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
277
430
  }
278
431
 
279
432
  const taskId = String(params.taskId ?? "");
280
- const progress = String(params.progress ?? "");
433
+ const progress = typeof params.progress === "string" ? params.progress : "";
281
434
  const status = typeof params.status === "string" ? params.status : undefined;
282
435
 
283
436
  if (!taskId) {
284
437
  return { content: [{ type: "text" as const, text: "taskId is required." }] };
285
438
  }
439
+ if (!progress && typeof params.summary !== "string") {
440
+ return { content: [{ type: "text" as const, text: "progress or summary is required." }] };
441
+ }
286
442
  if (status && !ALLOWED_PROGRESS_STATUSES.has(status)) {
287
443
  return {
288
444
  content: [{
@@ -293,10 +449,23 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
293
449
  }
294
450
 
295
451
  try {
296
- const patch: Record<string, unknown> = { progress };
452
+ const progressContract = normalizeWorkerProgressContract({
453
+ version: "1.0",
454
+ summary: typeof params.summary === "string" ? params.summary : progress,
455
+ status,
456
+ currentStep: params.currentStep,
457
+ nextStep: params.nextStep,
458
+ blockers: params.blockers,
459
+ }) ?? backfillWorkerProgressContract(progress, status);
460
+ const patch: Record<string, unknown> = {
461
+ progress: renderWorkerProgressText(progressContract, progress),
462
+ };
297
463
  if (status) {
298
464
  patch.status = status;
299
465
  }
466
+ if (progressContract) {
467
+ patch.progressContract = progressContract;
468
+ }
300
469
 
301
470
  const res = await fetch(`${identity.controllerUrl}/api/v1/tasks/${taskId}`, {
302
471
  method: "PATCH",
@@ -11,10 +11,10 @@ export type WorkerServiceDeps = {
11
11
  config: PluginConfig;
12
12
  logger: PluginLogger;
13
13
  onIdentityEstablished: (identity: WorkerIdentity) => void;
14
- taskExecutor?: (taskDescription: string, taskId: string) => Promise<string>;
14
+ taskExecutor?: (taskDescription: string, assignment: TaskAssignmentPayload) => Promise<string>;
15
15
  prepareTaskAssignment?: (assignment: TaskAssignmentPayload) => Promise<void> | void;
16
16
  publishTaskAssignment?: (assignment: TaskAssignmentPayload, result: string) => Promise<void> | void;
17
- cancelTaskExecution?: (taskId: string) => Promise<boolean> | boolean;
17
+ cancelTaskExecution?: (taskId: string, sessionKey?: string) => Promise<boolean> | boolean;
18
18
  messageQueue?: MessageQueue;
19
19
  };
20
20
 
@@ -27,6 +27,7 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
27
27
  let controllerUrl: string | null = null;
28
28
  let workerId: string | null = null;
29
29
  let activeTaskId: string | undefined;
30
+ const activeTaskSessionKeys = new Map<string, string>();
30
31
  const cancelledTaskIds = new Set<string>();
31
32
 
32
33
  const taskExecutor = externalTaskExecutor
@@ -34,10 +35,11 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
34
35
  const taskId = assignment.taskId;
35
36
  cancelledTaskIds.delete(taskId);
36
37
  activeTaskId = taskId;
38
+ activeTaskSessionKeys.set(taskId, assignment.executionSessionKey || `teamclaw-task-${taskId}`);
37
39
  try {
38
40
  await deps.prepareTaskAssignment?.(assignment);
39
41
  const taskPrompt = [assignment.title.trim(), assignment.description.trim()].filter(Boolean).join("\n\n");
40
- const result = await externalTaskExecutor(taskPrompt, taskId);
42
+ const result = await externalTaskExecutor(taskPrompt, assignment);
41
43
  if (cancelledTaskIds.has(taskId)) {
42
44
  throw new Error("Task execution cancelled by controller");
43
45
  }
@@ -45,6 +47,7 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
45
47
  return result;
46
48
  } finally {
47
49
  activeTaskId = undefined;
50
+ activeTaskSessionKeys.delete(taskId);
48
51
  if (!cancelledTaskIds.has(taskId)) {
49
52
  cancelledTaskIds.delete(taskId);
50
53
  }
@@ -74,7 +77,7 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
74
77
 
75
78
  cancelledTaskIds.add(taskId);
76
79
  try {
77
- const cancelled = await deps.cancelTaskExecution?.(taskId);
80
+ const cancelled = await deps.cancelTaskExecution?.(taskId, activeTaskSessionKeys.get(taskId));
78
81
  return cancelled ?? true;
79
82
  } catch (err) {
80
83
  logger.warn(`Worker: failed to cancel task ${taskId}: ${err instanceof Error ? err.message : String(err)}`);
@@ -130,9 +133,9 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
130
133
  } else {
131
134
  controllerUrl = identity.controllerUrl;
132
135
  workerId = identity.workerId;
136
+ onIdentityEstablished(identity);
133
137
  // Restart server with worker ID and task executor
134
138
  await startServer();
135
- onIdentityEstablished(identity);
136
139
  }
137
140
 
138
141
  // Start heartbeat
@@ -142,8 +145,8 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
142
145
  if (newIdentity && !controllerUrl) {
143
146
  controllerUrl = newIdentity.controllerUrl;
144
147
  workerId = newIdentity.workerId;
145
- await startServer();
146
148
  onIdentityEstablished(newIdentity);
149
+ await startServer();
147
150
  }
148
151
  return;
149
152
  }