Skip to content

Commit ef0856b

Browse files
Merge remote-tracking branch 'upstream/main' into dev
2 parents 4959e33 + 8d8e2c3 commit ef0856b

5 files changed

Lines changed: 268 additions & 9 deletions

File tree

prd.json

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,182 @@
160160
],
161161
"passes": true,
162162
"priority": "P2"
163+
},
164+
{
165+
"id": "US-012",
166+
"title": "Trust prompt resolver with allowlist auto-trust",
167+
"description": "Add allowlisted auto-trust behavior for known repos/worktrees. Trust prompts currently block TUI startup and require manual intervention. Implement automatic trust resolution for pre-approved repositories.",
168+
"acceptanceCriteria": [
169+
"TrustAllowlist config structure with repo patterns",
170+
"Auto-trust behavior for allowlisted repos/worktrees",
171+
"trust_required event emitted when trust prompt detected",
172+
"trust_resolved event emitted when trust is granted",
173+
"Non-allowlisted repos remain gated (manual trust required)",
174+
"Integration with worker boot lifecycle",
175+
"Tests for allowlist matching and event emission"
176+
],
177+
"passes": true,
178+
"priority": "P1"
179+
},
180+
{
181+
"id": "US-013",
182+
"title": "Phase 2 - Session event ordering + terminal-state reconciliation",
183+
"description": "When the same session emits contradictory lifecycle events (idle, error, completed, transport/server-down) in close succession, expose deterministic final truth. Attach monotonic sequence/causal ordering metadata, classify terminal vs advisory events, reconcile duplicate/out-of-order terminal events into one canonical lane outcome.",
184+
"acceptanceCriteria": [
185+
"Monotonic sequence / causal ordering metadata attached to session lifecycle events",
186+
"Terminal vs advisory event classification implemented",
187+
"Reconcile duplicate or out-of-order terminal events into one canonical outcome",
188+
"Distinguish 'session terminal state unknown because transport died' from real 'completed'",
189+
"Tests verify reconciliation behavior with out-of-order event bursts"
190+
],
191+
"passes": true,
192+
"priority": "P1"
193+
},
194+
{
195+
"id": "US-014",
196+
"title": "Phase 2 - Event provenance / environment labeling",
197+
"description": "Every emitted event should declare its source (live_lane, test, healthcheck, replay, transport) so claws do not mistake test noise for production truth. Include environment/channel label, emitter identity, and confidence/trust level.",
198+
"acceptanceCriteria": [
199+
"EventProvenance enum with live_lane, test, healthcheck, replay, transport variants",
200+
"Environment/channel label attached to all events",
201+
"Emitter identity field on events",
202+
"Confidence/trust level field for downstream automation",
203+
"Tests verify provenance labeling and filtering"
204+
],
205+
"passes": true,
206+
"priority": "P1"
207+
},
208+
{
209+
"id": "US-015",
210+
"title": "Phase 2 - Session identity completeness at creation time",
211+
"description": "A newly created session should emit stable title, workspace/worktree path, and lane/session purpose at creation time. If any field is not yet known, emit explicit typed placeholder reason rather than bare unknown string.",
212+
"acceptanceCriteria": [
213+
"Session creation emits stable title, workspace/worktree path, purpose immediately",
214+
"Explicit typed placeholder when fields unknown (not bare 'unknown' strings)",
215+
"Later-enriched metadata reconciles onto same session identity without ambiguity",
216+
"Tests verify session identity completeness and placeholder handling"
217+
],
218+
"passes": true,
219+
"priority": "P1"
220+
},
221+
{
222+
"id": "US-016",
223+
"title": "Phase 2 - Duplicate terminal-event suppression",
224+
"description": "When the same session emits repeated completed/failed/terminal notifications, collapse duplicates before they trigger repeated downstream reactions. Attach canonical terminal-event fingerprint per lane/session outcome.",
225+
"acceptanceCriteria": [
226+
"Canonical terminal-event fingerprint attached per lane/session outcome",
227+
"Suppress/coalesce repeated terminal notifications within reconciliation window",
228+
"Preserve raw event history for audit while exposing one actionable outcome downstream",
229+
"Surface when later duplicate materially differs from original terminal payload",
230+
"Tests verify deduplication and material difference detection"
231+
],
232+
"passes": true,
233+
"priority": "P2"
234+
},
235+
{
236+
"id": "US-017",
237+
"title": "Phase 2 - Lane ownership / scope binding",
238+
"description": "Each session and lane event should declare who owns it and what workflow scope it belongs to. Attach owner/assignee identity, workflow scope (claw-code-dogfood, external-git-maintenance, infra-health, manual-operator), and mark whether watcher is expected to act, observe only, or ignore.",
239+
"acceptanceCriteria": [
240+
"Owner/assignee identity attached to sessions and lane events",
241+
"Workflow scope field (claw-code-dogfood, external-git-maintenance, etc.)",
242+
"Watcher action expectation field (act, observe-only, ignore)",
243+
"Preserve scope through session restarts, resumes, and late terminal events",
244+
"Tests verify ownership and scope binding"
245+
],
246+
"passes": true,
247+
"priority": "P2"
248+
},
249+
{
250+
"id": "US-018",
251+
"title": "Phase 2 - Nudge acknowledgment / dedupe contract",
252+
"description": "Periodic clawhip nudges should carry nudge id/cycle id and delivery timestamp. Expose whether claw has already acknowledged or responded for that cycle. Distinguish new nudge, retry nudge, and stale duplicate.",
253+
"acceptanceCriteria": [
254+
"Nudge id / cycle id and delivery timestamp attached",
255+
"Acknowledgment state exposed (already acknowledged or not)",
256+
"Distinguish new nudge vs retry nudge vs stale duplicate",
257+
"Allow downstream summaries to bind reported pinpoint back to triggering nudge id",
258+
"Tests verify nudge deduplication and acknowledgment tracking"
259+
],
260+
"passes": true,
261+
"priority": "P2"
262+
},
263+
{
264+
"id": "US-019",
265+
"title": "Phase 2 - Stable roadmap-id assignment for newly filed pinpoints",
266+
"description": "When a claw records a new pinpoint/follow-up, assign or expose a stable tracking id immediately. Expose that id in structured event/report payload and preserve across edits, reorderings, and summary compression.",
267+
"acceptanceCriteria": [
268+
"Canonical roadmap id assigned at filing time",
269+
"Roadmap id exposed in structured event/report payload",
270+
"Same id preserved across edits, reorderings, summary compression",
271+
"Distinguish 'new roadmap filing' from 'update to existing roadmap item'",
272+
"Tests verify stable id assignment and update detection"
273+
],
274+
"passes": true,
275+
"priority": "P2"
276+
},
277+
{
278+
"id": "US-020",
279+
"title": "Phase 2 - Roadmap item lifecycle state contract",
280+
"description": "Each roadmap pinpoint should carry machine-readable lifecycle state (filed, acknowledged, in_progress, blocked, done, superseded). Attach last state-change timestamp and preserve lineage when one pinpoint supersedes or merges into another.",
281+
"acceptanceCriteria": [
282+
"Lifecycle state enum with filed, acknowledged, in_progress, blocked, done, superseded",
283+
"Last state-change timestamp attached",
284+
"New report can declare first filing, status update, or closure",
285+
"Preserve lineage when one pinpoint supersedes or merges into another",
286+
"Tests verify lifecycle state transitions"
287+
],
288+
"passes": true,
289+
"priority": "P2"
290+
},
291+
{
292+
"id": "US-021",
293+
"title": "Request body size pre-flight check for OpenAI-compatible provider",
294+
"description": "Implement pre-flight request body size estimation to prevent 400 Bad Request errors from API gateways with size limits. Based on dogfood findings with kimi-k2.5 testing, DashScope API has a 6MB request body limit that was exceeded by large system prompts.",
295+
"acceptanceCriteria": [
296+
"Pre-flight size estimation before sending requests to OpenAI-compatible providers",
297+
"Clear error message when request exceeds provider-specific size limit",
298+
"Configuration for different provider limits (6MB DashScope, 100MB OpenAI, etc.)",
299+
"Unit tests for size estimation and limit checking",
300+
"Integration with existing error handling for actionable user messages"
301+
],
302+
"passes": true,
303+
"priority": "P1"
304+
},
305+
{
306+
"id": "US-022",
307+
"title": "Enhanced error context for API failures",
308+
"description": "Add structured error context to API failures including request ID tracking across retries, provider-specific error code mapping, and suggested user actions based on error type (e.g., 'Reduce prompt size' for 413, 'Check API key' for 401).",
309+
"acceptanceCriteria": [
310+
"Request ID tracking across retries with full context in error messages",
311+
"Provider-specific error code mapping with actionable suggestions",
312+
"Suggested user actions for common error types (401, 403, 413, 429, 500, 502-504)",
313+
"Unit tests for error context extraction",
314+
"All existing tests pass and clippy is clean"
315+
],
316+
"passes": true,
317+
"priority": "P1"
318+
},
319+
{
320+
"id": "US-023",
321+
"title": "Add automatic routing for kimi models to DashScope",
322+
"description": "Based on dogfood findings with kimi-k2.5 testing, users must manually prefix with dashscope/kimi-k2.5 instead of just using kimi-k2.5. Add automatic routing for kimi/ and kimi- prefixed models to DashScope (similar to qwen models), and add a 'kimi' alias to the model registry.",
323+
"acceptanceCriteria": [
324+
"kimi/ and kimi- prefix routing to DashScope in metadata_for_model()",
325+
"'kimi' alias in MODEL_REGISTRY that resolves to 'kimi-k2.5'",
326+
"resolve_model_alias() handles the kimi alias correctly",
327+
"Unit tests for kimi routing (similar to qwen routing tests)",
328+
"All tests pass and clippy is clean"
329+
],
330+
"passes": true,
331+
"priority": "P1"
163332
}
164-
]
333+
],
334+
"metadata": {
335+
"lastUpdated": "2026-04-16",
336+
"completedStories": ["US-001", "US-002", "US-003", "US-004", "US-005", "US-006", "US-007", "US-008", "US-009", "US-010", "US-011", "US-012", "US-013", "US-014", "US-015", "US-016", "US-017", "US-018", "US-019", "US-020", "US-021", "US-022", "US-023"],
337+
"inProgressStories": [],
338+
"totalStories": 23,
339+
"status": "completed"
340+
}
165341
}

rust/crates/api/src/error.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ pub enum ApiError {
5353
request_id: Option<String>,
5454
body: String,
5555
retryable: bool,
56+
/// Suggested user action based on error type (e.g., "Reduce prompt size" for 413)
57+
suggested_action: Option<String>,
5658
},
5759
RetriesExhausted {
5860
attempts: u32,
@@ -239,6 +241,7 @@ impl ApiError {
239241
}
240242

241243
impl Display for ApiError {
244+
#[allow(clippy::too_many_lines)]
242245
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
243246
match self {
244247
Self::MissingCredentials {
@@ -340,9 +343,7 @@ impl Display for ApiError {
340343
provider,
341344
} => write!(
342345
f,
343-
"request body size ({} bytes) exceeds {provider} limit ({} bytes); reduce prompt length or context before retrying",
344-
estimated_bytes,
345-
max_bytes
346+
"request body size ({estimated_bytes} bytes) exceeds {provider} limit ({max_bytes} bytes); reduce prompt length or context before retrying"
346347
),
347348
}
348349
}
@@ -489,6 +490,7 @@ mod tests {
489490
request_id: Some("req_jobdori_123".to_string()),
490491
body: String::new(),
491492
retryable: true,
493+
suggested_action: None,
492494
};
493495

494496
assert!(error.is_generic_fatal_wrapper());
@@ -511,6 +513,7 @@ mod tests {
511513
request_id: Some("req_nested_456".to_string()),
512514
body: String::new(),
513515
retryable: true,
516+
suggested_action: None,
514517
}),
515518
};
516519

@@ -531,6 +534,7 @@ mod tests {
531534
request_id: Some("req_ctx_123".to_string()),
532535
body: String::new(),
533536
retryable: false,
537+
suggested_action: None,
534538
};
535539

536540
assert!(error.is_context_window_failure());

rust/crates/api/src/providers/anthropic.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,7 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
885885
request_id,
886886
body,
887887
retryable,
888+
suggested_action: None,
888889
})
889890
}
890891

@@ -909,6 +910,7 @@ fn enrich_bearer_auth_error(error: ApiError, auth: &AuthSource) -> ApiError {
909910
request_id,
910911
body,
911912
retryable,
913+
suggested_action,
912914
} = error
913915
else {
914916
return error;
@@ -921,6 +923,7 @@ fn enrich_bearer_auth_error(error: ApiError, auth: &AuthSource) -> ApiError {
921923
request_id,
922924
body,
923925
retryable,
926+
suggested_action,
924927
};
925928
}
926929
let Some(bearer_token) = auth.bearer_token() else {
@@ -931,6 +934,7 @@ fn enrich_bearer_auth_error(error: ApiError, auth: &AuthSource) -> ApiError {
931934
request_id,
932935
body,
933936
retryable,
937+
suggested_action,
934938
};
935939
};
936940
if !bearer_token.starts_with("sk-ant-") {
@@ -941,6 +945,7 @@ fn enrich_bearer_auth_error(error: ApiError, auth: &AuthSource) -> ApiError {
941945
request_id,
942946
body,
943947
retryable,
948+
suggested_action,
944949
};
945950
}
946951
// Only append the hint when the AuthSource is pure BearerToken. If both
@@ -955,6 +960,7 @@ fn enrich_bearer_auth_error(error: ApiError, auth: &AuthSource) -> ApiError {
955960
request_id,
956961
body,
957962
retryable,
963+
suggested_action,
958964
};
959965
}
960966
let enriched_message = match message {
@@ -968,6 +974,7 @@ fn enrich_bearer_auth_error(error: ApiError, auth: &AuthSource) -> ApiError {
968974
request_id,
969975
body,
970976
retryable,
977+
suggested_action,
971978
}
972979
}
973980

@@ -1555,6 +1562,7 @@ mod tests {
15551562
request_id: Some("req_varleg_001".to_string()),
15561563
body: String::new(),
15571564
retryable: false,
1565+
suggested_action: None,
15581566
};
15591567

15601568
// when
@@ -1595,6 +1603,7 @@ mod tests {
15951603
request_id: None,
15961604
body: String::new(),
15971605
retryable: true,
1606+
suggested_action: None,
15981607
};
15991608

16001609
// when
@@ -1623,6 +1632,7 @@ mod tests {
16231632
request_id: None,
16241633
body: String::new(),
16251634
retryable: false,
1635+
suggested_action: None,
16261636
};
16271637

16281638
// when
@@ -1650,6 +1660,7 @@ mod tests {
16501660
request_id: None,
16511661
body: String::new(),
16521662
retryable: false,
1663+
suggested_action: None,
16531664
};
16541665

16551666
// when
@@ -1674,6 +1685,7 @@ mod tests {
16741685
request_id: None,
16751686
body: String::new(),
16761687
retryable: false,
1688+
suggested_action: None,
16771689
};
16781690

16791691
// when

rust/crates/api/src/providers/mod.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ const MODEL_REGISTRY: &[(&str, ProviderMetadata)] = &[
123123
default_base_url: openai_compat::DEFAULT_XAI_BASE_URL,
124124
},
125125
),
126+
(
127+
"kimi",
128+
ProviderMetadata {
129+
provider: ProviderKind::OpenAi,
130+
auth_env: "DASHSCOPE_API_KEY",
131+
base_url_env: "DASHSCOPE_BASE_URL",
132+
default_base_url: openai_compat::DEFAULT_DASHSCOPE_BASE_URL,
133+
},
134+
),
126135
];
127136

128137
#[must_use]
@@ -570,6 +579,34 @@ mod tests {
570579
);
571580
}
572581

582+
#[test]
583+
fn kimi_prefix_routes_to_dashscope() {
584+
// Kimi models via DashScope (kimi-k2.5, kimi-k1.5, etc.)
585+
let meta = super::metadata_for_model("kimi-k2.5")
586+
.expect("kimi-k2.5 must resolve to DashScope metadata");
587+
assert_eq!(meta.auth_env, "DASHSCOPE_API_KEY");
588+
assert_eq!(meta.base_url_env, "DASHSCOPE_BASE_URL");
589+
assert!(meta.default_base_url.contains("dashscope.aliyuncs.com"));
590+
assert_eq!(meta.provider, ProviderKind::OpenAi);
591+
592+
// With provider prefix
593+
let meta2 = super::metadata_for_model("kimi/kimi-k2.5")
594+
.expect("kimi/kimi-k2.5 must resolve to DashScope metadata");
595+
assert_eq!(meta2.auth_env, "DASHSCOPE_API_KEY");
596+
assert_eq!(meta2.provider, ProviderKind::OpenAi);
597+
598+
// Different kimi variants
599+
let meta3 = super::metadata_for_model("kimi-k1.5")
600+
.expect("kimi-k1.5 must resolve to DashScope metadata");
601+
assert_eq!(meta3.auth_env, "DASHSCOPE_API_KEY");
602+
}
603+
604+
#[test]
605+
fn kimi_alias_resolves_to_kimi_k2_5() {
606+
assert_eq!(super::resolve_model_alias("kimi"), "kimi-k2.5");
607+
assert_eq!(super::resolve_model_alias("KIMI"), "kimi-k2.5"); // case insensitive
608+
}
609+
573610
#[test]
574611
fn keeps_existing_max_token_heuristic() {
575612
assert_eq!(max_tokens_for_model("opus"), 32_000);

0 commit comments

Comments
 (0)