@tea-agent/loop-agent 0.43.0-next.5 → 0.43.0-next.6

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,8 @@
10
10
 
11
11
  ### 改进
12
12
 
13
+ - 修复模型续跑取消后带部分文字仍被标记成功的问题;前端 Plan 的取消、认证、限流及策略失败优先于容量恢复,精确格式错误保留原修复路径。完整性检查按需求建立单次索引,减少大计划重复扫描,同时拆分纯检查与恢复策略模块。
14
+
13
15
  - 优化 Operator Chat 长会话的流式渲染:完成回合、消息行、工具卡和用户输入锚点建立稳定的 memo 边界,流式文本更新不再重复渲染整段历史或重装锚点测量监听;缓存会在历史消息或工具实际更新时正确失效;无新增折叠回合时不再提交空状态及重复写入本地存储,工具参数与结果格式化按真实输入变化缓存。
14
16
  - 对话页「过程折叠」对齐 DSH 细节:折叠后的内容仍保留在页面中,浏览器 Ctrl+F 可直接命中并自动展开;折叠行展开时不再多出底部空隙;箭头改为 16px、颜色更淡的固定图标(靠旋转而非换图标),并补齐系统「减少动态效果」偏好;汇总文案新增「N 个 subagent」,内置 Explore 子代理不再计入普通工具调用。
15
17
  - 恢复前端 Plan 实时事实分页读取、逐需求 UX 遗漏检查与已完成范围复用;超限时补齐剩余验证目标,并接通 DAG 到 SDK 的原会话续跑。局部状态修改保留其他需求的关联,沿用耐久账本、显式替换和冻结分片边界。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.43.0-next.5",
4
- "gitSha": "6a8572a4f425eb430ee85547653c581987b6c0d2",
5
- "builtAt": "2026-09-09T06:17:31.024Z"
3
+ "version": "0.43.0-next.6",
4
+ "gitSha": "086f94db72547618b8dceb905a3972164acf56dd",
5
+ "builtAt": "2026-09-09T06:46:29.301Z"
6
6
  }
@@ -1,3 +1,6 @@
1
+ import { collectFrontendPlanMissingFacts, collectFrontendPlanPhaseMissingFacts, committedFactFromPlanRecord, planFactStringList, planFactScopeIntersects } from "../workflows/dag/frontend-plan-completeness.js";
2
+ export { collectFrontendPlanMissingFacts, collectFrontendPlanPhaseMissingFacts } from "../workflows/dag/frontend-plan-completeness.js";
3
+ import { classifyFrontendPlanRecovery } from "../workflows/dag/frontend-plan-recovery-policy.js";
1
4
  import { collectFrontendExecutionGroups, frontendExecutionSchema } from "../workflows/dag/frontend-execution-groups.js";
2
5
  import { FRONTEND_SCOPE_TARGET_BYTES, packFrontendInputUnits, parseFrontendInputBlock, projectFrontendContractPrompt, projectFrontendInputScope } from "../workflows/dag/frontend-input-projection.js";
3
6
  import { createDurableFrontendTools } from "../workflows/dag/frontend-durable-tools.js";
@@ -268,16 +271,6 @@ export function isPlannerThinkingExhausted(result, committedAnyFacts) {
268
271
  return false;
269
272
  return true;
270
273
  }
271
- /** Capacity recovery must not depend on whether a gateway emitted prose or
272
- * diagnostics. Precise provider/governance failures retain their ownership. */
273
- function isFrontendPlanCapacityStop(result) {
274
- if (result.ok || result.timedOut)
275
- return false;
276
- if (result.failureCategory === OUTPUT_LIMIT_RETRY_CATEGORY)
277
- return true;
278
- return readWriterThinkingExhaustionEvidence(result).stopReason === "length" &&
279
- (!result.failureCategory || ["empty-output", "network", "nonzero-exit", "unknown"].includes(result.failureCategory));
280
- }
281
274
  /**
282
275
  * The writer session burned an excessive token budget (a read-edit-test loop
283
276
  * that never converged) and still failed. Distinct from empty-output so the
@@ -3811,180 +3804,6 @@ const FRONTEND_PLAN_SEGMENTS = [
3811
3804
  ].join(" "),
3812
3805
  },
3813
3806
  ];
3814
- function committedFactFromPlanRecord(value) {
3815
- if (!value || typeof value !== "object" || Array.isArray(value))
3816
- return undefined;
3817
- const record = value;
3818
- if (record.phase !== undefined && record.phase !== "committed")
3819
- return undefined;
3820
- const fact = record.fact;
3821
- return fact && typeof fact === "object" && !Array.isArray(fact)
3822
- ? fact
3823
- : typeof record.kind === "string"
3824
- ? record
3825
- : undefined;
3826
- }
3827
- function planFactStringList(value) {
3828
- if (!Array.isArray(value))
3829
- return [];
3830
- return value.filter((item) => typeof item === "string" && item.trim().length > 0);
3831
- }
3832
- function planFactScopeIntersects(fact, requirementIds) {
3833
- return planFactStringList(fact.scopeRequirementIds).some((id) => requirementIds.has(id));
3834
- }
3835
- /** Compute the authoritative coverage queue from the committed plan ledger. */
3836
- export function collectFrontendPlanMissingFacts(input) {
3837
- const requirements = new Map();
3838
- const standaloneEvidenceGaps = new Set();
3839
- const verificationTargetIds = new Set();
3840
- const verificationTargetRequirements = new Map();
3841
- for (const value of input.committedFacts) {
3842
- const fact = committedFactFromPlanRecord(value);
3843
- if (!fact || fact.origin !== "plan")
3844
- continue;
3845
- if (fact.kind === "plan-requirement" && fact.entry && typeof fact.entry === "object") {
3846
- const entry = fact.entry;
3847
- if (typeof entry.id === "string" && entry.id.trim())
3848
- requirements.set(entry.id, entry);
3849
- }
3850
- if (fact.kind === "plan-verification-target" && fact.entry && typeof fact.entry === "object") {
3851
- const entry = fact.entry;
3852
- const id = entry.id;
3853
- if (typeof id === "string" && id.trim()) {
3854
- verificationTargetIds.add(id);
3855
- verificationTargetRequirements.set(id, new Set(Array.isArray(entry.requirementIds)
3856
- ? entry.requirementIds.filter((value) => typeof value === "string")
3857
- : []));
3858
- }
3859
- }
3860
- if (fact.kind === "plan-evidence-gap" && fact.entry && typeof fact.entry === "object") {
3861
- const entry = fact.entry;
3862
- const requirementId = entry.requirementId;
3863
- const description = entry.description;
3864
- if (typeof requirementId === "string" && requirementId.trim() && typeof description === "string" && description.trim()) {
3865
- standaloneEvidenceGaps.add(requirementId);
3866
- }
3867
- }
3868
- }
3869
- const missing = [];
3870
- for (const id of input.requirementIds) {
3871
- const entry = requirements.get(id);
3872
- if (!entry) {
3873
- missing.push({
3874
- kind: "plan-requirement",
3875
- id,
3876
- requirementIds: [id],
3877
- reason: `requirement ${id} has no committed plan-requirement fact`,
3878
- });
3879
- continue;
3880
- }
3881
- const targetIds = Array.isArray(entry.verificationTargetIds)
3882
- ? entry.verificationTargetIds.filter((value) => typeof value === "string" && value.trim().length > 0)
3883
- : [];
3884
- const gap = entry.evidenceGap && typeof entry.evidenceGap === "object"
3885
- ? entry.evidenceGap
3886
- : undefined;
3887
- const hasEvidenceGap = (typeof gap?.description === "string" && gap.description.trim().length > 0) ||
3888
- standaloneEvidenceGaps.has(id);
3889
- if (targetIds.length === 0 && !hasEvidenceGap) {
3890
- missing.push({
3891
- kind: "plan-verification-target",
3892
- requirementIds: [id],
3893
- reason: `requirement ${id} declares neither a verification target nor a non-empty evidenceGap`,
3894
- });
3895
- continue;
3896
- }
3897
- for (const targetId of targetIds) {
3898
- if (!verificationTargetIds.has(targetId) ||
3899
- !verificationTargetRequirements.get(targetId)?.has(id)) {
3900
- missing.push({
3901
- kind: "plan-verification-target",
3902
- id: targetId,
3903
- requirementIds: [id],
3904
- reason: `requirement ${id} references verification target ${targetId}, but that target is not committed`,
3905
- });
3906
- }
3907
- }
3908
- }
3909
- return missing;
3910
- }
3911
- /** Completeness checks for phases whose facts are committed incrementally. */
3912
- export function collectFrontendPlanPhaseMissingFacts(input) {
3913
- const facts = input.committedFacts
3914
- .map(committedFactFromPlanRecord)
3915
- .filter((fact) => Boolean(fact && fact.origin === "plan"));
3916
- if (input.phase === "ux-registry") {
3917
- return facts.some((fact) => fact.kind === "state-registry")
3918
- ? []
3919
- : [
3920
- {
3921
- kind: "state-registry",
3922
- requirementIds: [...input.requirementIds],
3923
- reason: "global UX vocabulary phase has no committed state-registry fact",
3924
- },
3925
- ];
3926
- }
3927
- if (input.phase === "ux-local") {
3928
- if (input.requirementIds.length > 1) {
3929
- return input.requirementIds.flatMap(id => collectFrontendPlanPhaseMissingFacts({
3930
- ...input, requirementIds: [id],
3931
- }));
3932
- }
3933
- const needsUx = input.requirementIds.some((id) => input.behaviorRequiredRequirementIds?.includes(id));
3934
- if (!needsUx)
3935
- return [];
3936
- const requirementSlice = new Set(input.requirementIds);
3937
- const scopedFacts = facts.filter((fact) => planFactScopeIntersects(fact, requirementSlice));
3938
- const hasChoice = scopedFacts.some((fact) => fact.kind === "component-choice" &&
3939
- Array.isArray(fact.uiComponentChoices) &&
3940
- fact.uiComponentChoices.length > 0);
3941
- const canonicalStateFlow = collectCanonicalStateFlowNames(scopedFacts);
3942
- const liveStateFlow = collectCanonicalStateFlowNames(facts);
3943
- const hasStateFlow = [...canonicalStateFlow.uiStateNames].some(name => liveStateFlow.uiStateNames.has(name)) ||
3944
- [...canonicalStateFlow.interactionNames].some(name => liveStateFlow.interactionNames.has(name));
3945
- const missing = [];
3946
- if (!hasChoice) {
3947
- missing.push({
3948
- kind: "component-choice",
3949
- requirementIds: [...input.requirementIds],
3950
- reason: "behaviour-required UX slice has no committed component-choice fact",
3951
- });
3952
- }
3953
- if (!hasStateFlow) {
3954
- missing.push({
3955
- kind: "state-flow",
3956
- requirementIds: [...input.requirementIds],
3957
- reason: "behaviour-required UX slice has no committed state-flow fact",
3958
- });
3959
- }
3960
- return missing;
3961
- }
3962
- const hasMockApi = facts.some((fact) => fact.kind === "mock-api");
3963
- const allInteractions = collectCanonicalStateFlowNames(input.committedFacts).interactionNames;
3964
- const liveInteractions = new Set([...collectCanonicalStateFlowNames(facts.filter(f => !planFactStringList(f.scopeRequirementIds).length || planFactScopeIntersects(f, new Set(input.requirementIds)))).interactionNames].filter(name => allInteractions.has(name)));
3965
- const coveredInteractions = new Set(facts
3966
- .filter((fact) => fact.kind === "data-flow")
3967
- .flatMap((fact) => planFactStringList(fact.interactions)));
3968
- const missing = [];
3969
- if (!hasMockApi) {
3970
- missing.push({
3971
- kind: "mock-api",
3972
- requirementIds: [...input.requirementIds],
3973
- reason: "global Mock/data phase has no committed mock-api fact",
3974
- });
3975
- }
3976
- for (const interaction of liveInteractions) {
3977
- if (coveredInteractions.has(interaction))
3978
- continue;
3979
- missing.push({
3980
- kind: "data-flow",
3981
- id: interaction,
3982
- requirementIds: [...input.requirementIds],
3983
- reason: `interaction ${interaction} has no committed data-flow fact`,
3984
- });
3985
- }
3986
- return missing;
3987
- }
3988
3807
  /** Estimate calls conservatively: requirement + one VT, with a second VT
3989
3808
  * reserved for behaviour-required requirements. Explicit declarations win. */
3990
3809
  export function estimateFrontendPlanRequirementRecordCalls(fact) {
@@ -5069,13 +4888,13 @@ export async function runFrontendPlanSegmentedSessions(input) {
5069
4888
  // ownership; an unrelated/global fact must not prove a slice complete.
5070
4889
  if (input.attempt > 1 && input.committedFacts && session.id.startsWith("ux-local-") && session.requirementSlice?.length) {
5071
4890
  const facts = input.committedFacts();
5072
- const complete = session.requirementSlice.every(id => {
5073
- const owned = facts.filter(value => {
5074
- const fact = committedFactFromPlanRecord(value);
5075
- return fact && planFactStringList(fact.scopeRequirementIds).includes(id);
5076
- });
5077
- return owned.length > 0 && collectFrontendPlanPhaseMissingFacts({ phase: "ux-local", requirementIds: [id], committedFacts: facts, behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds }).length === 0;
5078
- });
4891
+ const ownedIds = new Set(facts.flatMap(value => {
4892
+ const fact = committedFactFromPlanRecord(value);
4893
+ return fact ? planFactStringList(fact.scopeRequirementIds) : [];
4894
+ }));
4895
+ const complete = session.requirementSlice.every(id => ownedIds.has(id)) &&
4896
+ collectFrontendPlanPhaseMissingFacts({ phase: "ux-local", requirementIds: session.requirementSlice,
4897
+ committedFacts: facts, behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds }).length === 0;
5079
4898
  if (complete) {
5080
4899
  index += 1;
5081
4900
  continue;
@@ -5278,9 +5097,10 @@ export async function runFrontendPlanSegmentedSessions(input) {
5278
5097
  : isUxLocalSession
5279
5098
  ? buildUxPrompt(session.requirementSlice ?? [], missing)
5280
5099
  : buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds);
5281
- if (result.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(result.failureCategory))
5100
+ const recovery = classifyFrontendPlanRecovery({ ...result, stopReason: readWriterThinkingExhaustionEvidence(result).stopReason });
5101
+ if (recovery === "stop")
5282
5102
  return { ...result, ok: false };
5283
- if ((isFrontendPlanCapacityStop(result) || (session.atomicRecovery && result.ok)) && input.committedFacts &&
5103
+ if ((recovery === "output" || (session.atomicRecovery && result.ok)) && input.committedFacts &&
5284
5104
  !(missingPhaseFacts.length === 1 && missingPhaseFacts[0].kind === "plan-requirement") &&
5285
5105
  (session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id.startsWith("global-mock-data"))) {
5286
5106
  if (missingPhaseFacts.length === 0) {
@@ -5317,7 +5137,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
5317
5137
  return { ...accumulated, ok: false, failureCategory: OUTPUT_LIMIT_RETRY_CATEGORY,
5318
5138
  stderr: `${accumulated.stderr}\nfrontend plan capacity recovery exhausted: preserve committed facts; phase=${session.id}; scope=${missingIds.join(",")}; strategy=atomic-fact; missing=${JSON.stringify(missingPhaseFacts)}`.trim() };
5319
5139
  }
5320
- const capacityExhausted = readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory);
5140
+ const capacityExhausted = recovery === "output" || recovery === "context";
5321
5141
  if (capacityExhausted && !result.timedOut) {
5322
5142
  const failure = () => mapPlannerExhaustion({ ...result, ok: false, stderr: `${result.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${session.id}; no smaller complete scope can finish; unchanged retries are disabled` }, committedAfter > committedBefore);
5323
5143
  const missingCoverageIds = input.committedFacts ? [...new Set(collectFrontendPlanMissingFacts({ requirementIds: session.coverageSlice ?? session.requirementSlice ?? allRequirementIds, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds))] : [...(session.coverageSlice ?? session.requirementSlice ?? allRequirementIds)];
@@ -5423,7 +5243,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
5423
5243
  : undefined;
5424
5244
  const zeroProgressBurn = coverageSlice !== undefined &&
5425
5245
  coverageSlice.length > 1 &&
5426
- (readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory) || (committedAfter === committedBefore && !(result.assistantText ?? "").trim() && !result.stderr.trim())) &&
5246
+ (capacityExhausted || (["empty-output", "unknown"].includes(result.failureCategory) && committedAfter === committedBefore && !(result.assistantText ?? "").trim() && !result.stderr.trim())) &&
5427
5247
  !result.timedOut;
5428
5248
  if (zeroProgressBurn && coverageSlice) {
5429
5249
  const missingIds = input.committedFacts ? new Set(collectFrontendPlanMissingFacts({ requirementIds: coverageSlice, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds)) : undefined;
@@ -978,6 +978,7 @@ async function executeSingleSdkAttemptInternal(options) {
978
978
  ? collected.parsedEvents
979
979
  : (fallbackParsed?.parsedEvents ?? 0);
980
980
  const tokensUsed = aggregateSdkTokenUsage(usageSamples);
981
+ const cancelled = stopReason === "aborted";
981
982
  const outputLimitExhausted = Boolean(options.outputLimitRecovery && stopReason === "length" && !committedTerminalObserved && !timedOut && !stderr);
982
983
  const failureCategory = !terminationConfirmed ? "termination-unconfirmed" : stopReason === "aborted" ? "cancelled" : outputLimitExhausted ? "output-limit" : providerBudgetFailure ? (providerBudgetFailure.includes("REQUEST_ABORTED") ? "interrupted" : providerBudgetFailure.startsWith("FRONTEND_CONTEXT_ESTIMATE_EXCEEDED") ? "context-budget-exhausted" : "budget_breach") : terminationConfirmed
983
984
  ? classifyPiFailure({
@@ -994,13 +995,14 @@ async function executeSingleSdkAttemptInternal(options) {
994
995
  backend: "sdk",
995
996
  command: ["pi-sdk", ...piSdkArgs],
996
997
  durationMs,
997
- exitCode: timedOut || stderr || outputLimitExhausted ? 1 : 0,
998
+ exitCode: timedOut || stderr || outputLimitExhausted || cancelled ? 1 : 0,
998
999
  failureCategory,
999
1000
  modelDisplay,
1000
1001
  ok: terminationConfirmed &&
1001
1002
  !timedOut &&
1002
1003
  !stderr &&
1003
1004
  !outputLimitExhausted &&
1005
+ !cancelled &&
1004
1006
  assistantText.length > 0 &&
1005
1007
  !collected.outputTooLarge,
1006
1008
  parsedEvents,
@@ -0,0 +1,186 @@
1
+ import { collectCanonicalStateFlowNames } from "./frontend-contract-facts.js";
2
+ export function committedFactFromPlanRecord(value) {
3
+ if (!value || typeof value !== "object" || Array.isArray(value))
4
+ return undefined;
5
+ const record = value;
6
+ if (record.phase !== undefined && record.phase !== "committed")
7
+ return undefined;
8
+ const fact = record.fact;
9
+ return fact && typeof fact === "object" && !Array.isArray(fact)
10
+ ? fact
11
+ : typeof record.kind === "string"
12
+ ? record
13
+ : undefined;
14
+ }
15
+ export function planFactStringList(value) {
16
+ if (!Array.isArray(value))
17
+ return [];
18
+ return value.filter((item) => typeof item === "string" && item.trim().length > 0);
19
+ }
20
+ export function planFactScopeIntersects(fact, requirementIds) {
21
+ return planFactStringList(fact.scopeRequirementIds).some((id) => requirementIds.has(id));
22
+ }
23
+ /** Compute the authoritative coverage queue from the committed plan ledger. */
24
+ export function collectFrontendPlanMissingFacts(input) {
25
+ const requirements = new Map();
26
+ const standaloneEvidenceGaps = new Set();
27
+ const verificationTargetIds = new Set();
28
+ const verificationTargetRequirements = new Map();
29
+ for (const value of input.committedFacts) {
30
+ const fact = committedFactFromPlanRecord(value);
31
+ if (!fact || fact.origin !== "plan")
32
+ continue;
33
+ if (fact.kind === "plan-requirement" && fact.entry && typeof fact.entry === "object") {
34
+ const entry = fact.entry;
35
+ if (typeof entry.id === "string" && entry.id.trim())
36
+ requirements.set(entry.id, entry);
37
+ }
38
+ if (fact.kind === "plan-verification-target" && fact.entry && typeof fact.entry === "object") {
39
+ const entry = fact.entry;
40
+ const id = entry.id;
41
+ if (typeof id === "string" && id.trim()) {
42
+ verificationTargetIds.add(id);
43
+ verificationTargetRequirements.set(id, new Set(Array.isArray(entry.requirementIds)
44
+ ? entry.requirementIds.filter((value) => typeof value === "string")
45
+ : []));
46
+ }
47
+ }
48
+ if (fact.kind === "plan-evidence-gap" && fact.entry && typeof fact.entry === "object") {
49
+ const entry = fact.entry;
50
+ const requirementId = entry.requirementId;
51
+ const description = entry.description;
52
+ if (typeof requirementId === "string" && requirementId.trim() && typeof description === "string" && description.trim()) {
53
+ standaloneEvidenceGaps.add(requirementId);
54
+ }
55
+ }
56
+ }
57
+ const missing = [];
58
+ for (const id of input.requirementIds) {
59
+ const entry = requirements.get(id);
60
+ if (!entry) {
61
+ missing.push({
62
+ kind: "plan-requirement",
63
+ id,
64
+ requirementIds: [id],
65
+ reason: `requirement ${id} has no committed plan-requirement fact`,
66
+ });
67
+ continue;
68
+ }
69
+ const targetIds = Array.isArray(entry.verificationTargetIds)
70
+ ? entry.verificationTargetIds.filter((value) => typeof value === "string" && value.trim().length > 0)
71
+ : [];
72
+ const gap = entry.evidenceGap && typeof entry.evidenceGap === "object"
73
+ ? entry.evidenceGap
74
+ : undefined;
75
+ const hasEvidenceGap = (typeof gap?.description === "string" && gap.description.trim().length > 0) ||
76
+ standaloneEvidenceGaps.has(id);
77
+ if (targetIds.length === 0 && !hasEvidenceGap) {
78
+ missing.push({
79
+ kind: "plan-verification-target",
80
+ requirementIds: [id],
81
+ reason: `requirement ${id} declares neither a verification target nor a non-empty evidenceGap`,
82
+ });
83
+ continue;
84
+ }
85
+ for (const targetId of targetIds) {
86
+ if (!verificationTargetIds.has(targetId) ||
87
+ !verificationTargetRequirements.get(targetId)?.has(id)) {
88
+ missing.push({
89
+ kind: "plan-verification-target",
90
+ id: targetId,
91
+ requirementIds: [id],
92
+ reason: `requirement ${id} references verification target ${targetId}, but that target is not committed`,
93
+ });
94
+ }
95
+ }
96
+ }
97
+ return missing;
98
+ }
99
+ /** Completeness checks for phases whose facts are committed incrementally. */
100
+ export function collectFrontendPlanPhaseMissingFacts(input) {
101
+ const facts = input.committedFacts
102
+ .map(committedFactFromPlanRecord)
103
+ .filter((fact) => Boolean(fact && fact.origin === "plan"));
104
+ if (input.phase === "ux-registry") {
105
+ return facts.some((fact) => fact.kind === "state-registry")
106
+ ? []
107
+ : [
108
+ {
109
+ kind: "state-registry",
110
+ requirementIds: [...input.requirementIds],
111
+ reason: "global UX vocabulary phase has no committed state-registry fact",
112
+ },
113
+ ];
114
+ }
115
+ if (input.phase === "ux-local") {
116
+ // One snapshot per check: no cross-revision cache to invalidate.
117
+ const required = new Set(input.behaviorRequiredRequirementIds ?? []);
118
+ const requested = new Set(input.requirementIds.filter(id => required.has(id)));
119
+ if (!requested.size)
120
+ return [];
121
+ const byRequirement = new Map();
122
+ for (const fact of facts) {
123
+ for (const id of new Set(planFactStringList(fact.scopeRequirementIds))) {
124
+ if (!requested.has(id))
125
+ continue;
126
+ const entries = byRequirement.get(id) ?? [];
127
+ entries.push(fact);
128
+ byRequirement.set(id, entries);
129
+ }
130
+ }
131
+ const liveStateFlow = collectCanonicalStateFlowNames(facts);
132
+ const missing = [];
133
+ for (const id of input.requirementIds) {
134
+ if (!required.has(id))
135
+ continue;
136
+ const scopedFacts = byRequirement.get(id) ?? [];
137
+ const hasChoice = scopedFacts.some((fact) => fact.kind === "component-choice" &&
138
+ Array.isArray(fact.uiComponentChoices) &&
139
+ fact.uiComponentChoices.length > 0);
140
+ const canonicalStateFlow = collectCanonicalStateFlowNames(scopedFacts);
141
+ const hasStateFlow = [...canonicalStateFlow.uiStateNames].some(name => liveStateFlow.uiStateNames.has(name)) ||
142
+ [...canonicalStateFlow.interactionNames].some(name => liveStateFlow.interactionNames.has(name));
143
+ if (!hasChoice) {
144
+ missing.push({
145
+ kind: "component-choice",
146
+ requirementIds: [id],
147
+ reason: "behaviour-required UX slice has no committed component-choice fact",
148
+ });
149
+ }
150
+ if (!hasStateFlow) {
151
+ missing.push({
152
+ kind: "state-flow",
153
+ requirementIds: [id],
154
+ reason: "behaviour-required UX slice has no committed state-flow fact",
155
+ });
156
+ }
157
+ }
158
+ return missing;
159
+ }
160
+ const hasMockApi = facts.some((fact) => fact.kind === "mock-api");
161
+ const allInteractions = collectCanonicalStateFlowNames(facts).interactionNames;
162
+ const requested = new Set(input.requirementIds);
163
+ const liveInteractions = new Set([...collectCanonicalStateFlowNames(facts.filter(f => !planFactStringList(f.scopeRequirementIds).length || planFactScopeIntersects(f, requested))).interactionNames].filter(name => allInteractions.has(name)));
164
+ const coveredInteractions = new Set(facts
165
+ .filter((fact) => fact.kind === "data-flow")
166
+ .flatMap((fact) => planFactStringList(fact.interactions)));
167
+ const missing = [];
168
+ if (!hasMockApi) {
169
+ missing.push({
170
+ kind: "mock-api",
171
+ requirementIds: [...input.requirementIds],
172
+ reason: "global Mock/data phase has no committed mock-api fact",
173
+ });
174
+ }
175
+ for (const interaction of liveInteractions) {
176
+ if (coveredInteractions.has(interaction))
177
+ continue;
178
+ missing.push({
179
+ kind: "data-flow",
180
+ id: interaction,
181
+ requirementIds: [...input.requirementIds],
182
+ reason: `interaction ${interaction} has no committed data-flow fact`,
183
+ });
184
+ }
185
+ return missing;
186
+ }
@@ -0,0 +1,18 @@
1
+ /** Precise failures own their recovery. A stale transport length marker must
2
+ * never turn cancellation, provider rejection or policy failure into progress. */
3
+ export function classifyFrontendPlanRecovery(result) {
4
+ const category = result.failureCategory ?? "";
5
+ if (result.timedOut || result.stopReason === "aborted" ||
6
+ /interrupt|termination-unconfirmed|budget_breach/.test(category) ||
7
+ ["cancelled", "auth", "missing-api-key", "rate-limit", "quota", "timeout", "tool-policy", "write-guard", "path-violation", "governance-blocked", "human-rejected"].includes(category))
8
+ return "stop";
9
+ if (result.ok)
10
+ return "none";
11
+ if (category === "output-limit")
12
+ return "output";
13
+ if (["context-overflow", "context-budget-exhausted"].includes(category))
14
+ return "context";
15
+ if (result.stopReason === "length" && ["", "empty-output", "network", "nonzero-exit", "unknown"].includes(category))
16
+ return "output";
17
+ return "none";
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.43.0-next.5",
3
+ "version": "0.43.0-next.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",