@tea-agent/loop-agent 0.24.7 → 0.24.8

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 CHANGED
@@ -10,6 +10,17 @@
10
10
 
11
11
  - 修复前端证据校验在 Windows 下通过长 `node -e` 命令执行时的终端转义崩溃:改为由 runtime 内部直接校验,兼容 CMD、Git Bash、PowerShell 与 Unix shell
12
12
 
13
+ ## [0.24.8] - 2026-07-29
14
+
15
+ ### 重点更新
16
+
17
+ - 修复 FINAL-VERIFY 改写已存在的 allowed_paths 证据(gitignore)时 Git checkpoint 误失败,以及 record-error 后 Task Pool 卡在 Running 无法 mark-failed/retry 的问题
18
+
19
+ ### 修复
20
+
21
+ - `finalizeGitTask` 对 allowed_paths 内的 pre-existing ignored evidence 允许 rewrite 并 force-add
22
+ - `runReadyTasks` 在 record-error 时 best-effort 投影 `runs.jsonl` + Failed 状态,便于官方 retry
23
+
13
24
  ## [0.24.7] - 2026-07-29
14
25
 
15
26
  ### 重点更新
@@ -101,11 +101,12 @@ export async function finalizeGitTask(transaction, outcome) {
101
101
  await assertTransactionPosition(transaction.repoRoot, current);
102
102
  const currentIgnored = await readIgnoredBaseline(transaction.repoRoot);
103
103
  const ignoredBaselineByPath = new Map(current.ignoredBaseline.map((entry) => [entry.path, entry.sha256]));
104
- const changedBaselineIgnored = currentIgnored.filter((entry) => ignoredBaselineByPath.has(entry.path) && ignoredBaselineByPath.get(entry.path) !== entry.sha256).map((entry) => entry.path);
105
- const missingBaselineIgnored = current.ignoredBaseline.filter((entry) => !currentIgnored.some((candidate) => candidate.path === entry.path)).map((entry) => entry.path);
106
- if (changedBaselineIgnored.length > 0 || missingBaselineIgnored.length > 0) {
107
- throw new Error(`pre-existing ignored file changed during Git transaction: ${[...changedBaselineIgnored, ...missingBaselineIgnored].join(", ")}`);
108
- }
104
+ const changedBaselineIgnored = currentIgnored
105
+ .filter((entry) => ignoredBaselineByPath.has(entry.path) && ignoredBaselineByPath.get(entry.path) !== entry.sha256)
106
+ .map((entry) => entry.path);
107
+ const missingBaselineIgnored = current.ignoredBaseline
108
+ .filter((entry) => !currentIgnored.some((candidate) => candidate.path === entry.path))
109
+ .map((entry) => entry.path);
109
110
  const existing = current.checkpoints.find((entry) => entry.workerRunId === outcome.workerRunId);
110
111
  if (outcome.status === "reused" || existing) {
111
112
  if (!existing)
@@ -123,12 +124,28 @@ export async function finalizeGitTask(transaction, outcome) {
123
124
  const newIgnored = currentIgnored.map((entry) => entry.path).filter((entry) => !ignoredBaselineByPath.has(entry));
124
125
  if (outcome.status === "succeeded") {
125
126
  const allowedPaths = expandAllowedPathsForWorkflow(resolveWorkflow(outcome.taskSpec).workflow, outcome.taskSpec.constraints.allowed_paths);
127
+ const isAllowedEvidencePath = (entry) => !isSensitivePath(entry) &&
128
+ !isEphemeralToolCachePath(entry) &&
129
+ allowedPaths.some((glob) => matchesGlob(entry, glob));
130
+ // Pre-existing ignored evidence under allowed_paths may be rewritten by
131
+ // FINAL-VERIFY smoke / verify-final and must force-add into the checkpoint.
132
+ // Other baseline ignored mutations remain fail-closed (e.g. dist/, secrets).
133
+ const blockedBaselineChanged = changedBaselineIgnored.filter((entry) => !isAllowedEvidencePath(entry));
134
+ const blockedBaselineMissing = missingBaselineIgnored.filter((entry) => !isEphemeralToolCachePath(entry) && !isAllowedEvidencePath(entry));
135
+ if (blockedBaselineChanged.length > 0 || blockedBaselineMissing.length > 0) {
136
+ throw new Error(`pre-existing ignored file changed during Git transaction: ${[...blockedBaselineChanged, ...blockedBaselineMissing].join(", ")}`);
137
+ }
126
138
  // Evidence under allowed_paths may be gitignored for local convenience (e.g.
127
139
  // reports/welcome/** smoke JSON) but must still checkpoint for Task Pool Done.
128
140
  // Sensitive / out-of-scope ignored files remain fail-closed; ephemeral tool
129
141
  // caches neither block nor force-add.
130
142
  const forceAddIgnored = [];
131
143
  const blockedIgnored = [];
144
+ const rewrittenBaselineEvidence = changedBaselineIgnored.filter((entry) => isAllowedEvidencePath(entry));
145
+ for (const entry of rewrittenBaselineEvidence) {
146
+ if (!forceAddIgnored.includes(entry))
147
+ forceAddIgnored.push(entry);
148
+ }
132
149
  for (const entry of newIgnored) {
133
150
  if (isEphemeralToolCachePath(entry))
134
151
  continue;
@@ -137,7 +154,8 @@ export async function finalizeGitTask(transaction, outcome) {
137
154
  continue;
138
155
  }
139
156
  if (allowedPaths.some((glob) => matchesGlob(entry, glob))) {
140
- forceAddIgnored.push(entry);
157
+ if (!forceAddIgnored.includes(entry))
158
+ forceAddIgnored.push(entry);
141
159
  }
142
160
  else {
143
161
  blockedIgnored.push(entry);
@@ -166,7 +184,10 @@ export async function finalizeGitTask(transaction, outcome) {
166
184
  await assertTransactionPosition(transaction.repoRoot, { ...current, lastCheckpoint: commit });
167
185
  await assertCheckpointMetadata(transaction.repoRoot, commit, outcome.taskSpec, outcome.workerRunId);
168
186
  const afterCommitIgnored = await readIgnoredBaseline(transaction.repoRoot);
169
- assertIgnoredBaselineUnchanged(current.ignoredBaseline.filter((entry) => !isEphemeralToolCachePath(entry.path)), afterCommitIgnored.filter((entry) => !isEphemeralToolCachePath(entry.path)));
187
+ // force-add may promote allowed evidence from ignored → tracked; drop those
188
+ // paths from the post-commit baseline equality check.
189
+ const forceAdded = new Set(forceAddIgnored);
190
+ assertIgnoredBaselineUnchanged(current.ignoredBaseline.filter((entry) => !isEphemeralToolCachePath(entry.path) && !forceAdded.has(entry.path)), afterCommitIgnored.filter((entry) => !isEphemeralToolCachePath(entry.path) && !forceAdded.has(entry.path)));
170
191
  }
171
192
  catch (error) {
172
193
  const branch = await git(transaction.repoRoot, ["branch", "--show-current"]).catch(() => "");
@@ -199,6 +220,10 @@ export async function finalizeGitTask(transaction, outcome) {
199
220
  changedFiles: checkpointFiles,
200
221
  };
201
222
  }
223
+ // Failed / keep-diff paths still fail closed on unexpected baseline ignored mutations.
224
+ if (changedBaselineIgnored.length > 0 || missingBaselineIgnored.length > 0) {
225
+ throw new Error(`pre-existing ignored file changed during Git transaction: ${[...changedBaselineIgnored, ...missingBaselineIgnored].join(", ")}`);
226
+ }
202
227
  const artifactDir = path.join(path.dirname(transaction.recordPath), "failures", outcome.workerRunId);
203
228
  await captureFailureArtifacts(transaction.repoRoot, artifactDir, changes, newIgnored, outcome);
204
229
  if (outcome.keepFailedDiff) {
@@ -304,11 +304,51 @@ export async function runReadyTasks(options) {
304
304
  if (result.status !== "succeeded") {
305
305
  await options.onTaskFinalized?.({ status: result.status, taskSpec, workerRunId: result.workerRunId, runRecordPath: result.runRecordPath });
306
306
  }
307
+ const recordErrorMessage = errorMessage(error);
308
+ // Always project a Pool run fact for record-error so operators can
309
+ // mark-failed / retry without hand-editing Running state.
310
+ try {
311
+ await recordTaskPoolRun({
312
+ repoRoot: options.repoRoot,
313
+ run: {
314
+ schemaVersion: 1,
315
+ batchRunId,
316
+ workerRunId: result.workerRunId,
317
+ taskId: result.businessId,
318
+ featureId: taskSpec.feature_id,
319
+ status: "run-error",
320
+ recordedAt: new Date().toISOString(),
321
+ error: recordErrorMessage,
322
+ runRecordPath: result.runRecordPath,
323
+ ...(result.workflow ? { workflow: result.workflow } : {}),
324
+ ...(controllerIdentity ? { controllerIdentity } : {}),
325
+ ...(retryOfWorkerRunId ? { retryOfWorkerRunId } : {}),
326
+ },
327
+ });
328
+ await writeTaskPoolState(options.repoRoot, {
329
+ schemaVersion: 2,
330
+ featureId: taskSpec.feature_id,
331
+ taskId,
332
+ status: "Failed",
333
+ updatedAt: new Date().toISOString(),
334
+ workerRunId: result.workerRunId,
335
+ lastRunRecordPath: result.runRecordPath,
336
+ failure: {
337
+ category: "EnvFailure",
338
+ recommendedFollowUpKind: "manual-review",
339
+ derivedFollowUpTaskId: `${taskId}-record-error-review`,
340
+ source: "report-decision",
341
+ },
342
+ });
343
+ }
344
+ catch {
345
+ // Best-effort; batch still reports record-error.
346
+ }
307
347
  tasks.push({
308
348
  taskId,
309
349
  workerRunId: result.workerRunId,
310
350
  status: "record-error",
311
- error: errorMessage(error),
351
+ error: recordErrorMessage,
312
352
  runRecordPath: result.runRecordPath,
313
353
  });
314
354
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.24.7",
3
+ "version": "0.24.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",