Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.d/temporal-context-stored-request-get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `GET /v1/temporal-context/{idempotency_key}/request` returns the stored LineageWeave temporal-context create request on `tepp-loopback` (ADR 0091). Metric-free of RMSE/`tepp.scientific_acceptance.v1`. `inference_status` on the live projection remains `temporal_association_only`. Naruon refused. `NaruonLiveService` stays POST-only. Does not re-open collection GET or cancel lineages. Not GAP-010 Figma/export, not persistence.
1 change: 1 addition & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin
| contextual-orchestrator interpretation port | [`docs/connectors/contextual-orchestrator-interpretation-port.md`](docs/connectors/contextual-orchestrator-interpretation-port.md) |
| Orchestrator live HTTP doctoring | [`docs/research/orchestrator-live-http.md`](docs/research/orchestrator-live-http.md) |
| Temporal-context GET-by-id doctoring | [`docs/research/temporal-context-retrieval-get.md`](docs/research/temporal-context-retrieval-get.md) |
| Temporal-context stored-request GET doctoring | [`docs/research/temporal-context-stored-request-get.md`](docs/research/temporal-context-stored-request-get.md) |
| UML/runtime/scientific flows | [`docs/UML.md`](docs/UML.md) |
| Logical/physical ERD | [`docs/ERD.md`](docs/ERD.md) |
| Security policy | [`SECURITY.md`](SECURITY.md) |
Expand Down
78 changes: 70 additions & 8 deletions crates/tepp_api/src/analysis_run_live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ use crate::{
ErrorEnvelope, NARUON_LIVE_IO_TIMEOUT, NaruonLiveResponse, PROJECT_HISTORY_PATH,
ProjectHistoryProjection, ProjectHistoryRequest, TEMPORAL_CONTEXT_PATH,
TEMPORAL_CONTEXT_RETRIEVAL_INFERENCE_STATUS, TemporalContextRequest, TemporalContextRetrieved,
build_temporal_context, project_history_projection, requests_are_idempotent_matches,
temporal_context_retrieval_path_id,
build_temporal_context, project_history_projection, refuse_metrics_on_temporal_context_stored_request_payload,
requests_are_idempotent_matches, temporal_context_retrieval_path_id,
temporal_context_stored_request_path_id,
};

const MAX_LIVE_REQUEST_BODY_BYTES: usize = DEFAULT_PROJECT_HISTORY_BYTE_LIMIT;
Expand All @@ -43,7 +44,7 @@ pub struct AnalysisRunLiveService {
next_request_serial: u64,
accepted_runs: HashMap<String, (AnalysisRunRequest, AnalysisRunAccepted)>,
accepted_project_histories: HashMap<String, (ProjectHistoryRequest, ProjectHistoryProjection)>,
accepted_temporal_contexts: HashMap<String, TemporalContextRetrieved>,
accepted_temporal_contexts: HashMap<String, (TemporalContextRequest, TemporalContextRetrieved)>,
}

impl Default for AnalysisRunLiveService {
Expand Down Expand Up @@ -149,6 +150,12 @@ impl AnalysisRunLiveService {
let (method, path) = parse_request_line(lines.next().unwrap_or(""))?;
let headers = parse_headers(&mut lines)?;
if method == "GET" {
if matches!(
temporal_context_stored_request_path_id(path),
Ok(_) | Err(ApiError::LimitExceeded)
) {
return self.get_temporal_context_stored_request(path, &headers, body);
}
return self.get_temporal_context(path, &headers, body);
}
if method != "POST"
Expand Down Expand Up @@ -189,12 +196,16 @@ impl AnalysisRunLiveService {
TEMPORAL_CONTEXT_RETRIEVAL_INFERENCE_STATUS,
)?;
let replay_key = format!("{consumer}\u{1f}{idempotency_key}");
if let Some(stored) = self.accepted_temporal_contexts.get(&replay_key) {
if stored.knowledge_cutoff != item.knowledge_cutoff {
if let Some((stored_request, stored)) = self.accepted_temporal_contexts.get(&replay_key)
{
if stored_request != &context_request
|| stored.knowledge_cutoff != item.knowledge_cutoff
{
return Err(ApiError::InvalidWirePayload);
}
} else {
self.accepted_temporal_contexts.insert(replay_key, item);
self.accepted_temporal_contexts
.insert(replay_key, (context_request.clone(), item));
Comment on lines +207 to +208

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Failed POSTs leave retrievable requests

When the generated response exceeds 64 KiB, insert stores the request before serialization returns an error. Both GET endpoints then expose a failed POST.

Prompt for agents
In crates/tepp_api/src/analysis_run_live.rs, accept_temporal_context mutates accepted_temporal_contexts before build_temporal_context and TemporalContextResponse::to_json complete. A temporal-context request can fit its 64 KiB input limit while the expanded response exceeds the same output limit, causing POST to return LimitExceeded after the registry has already changed. Reorder the operation so all response construction and serialization succeeds before inserting a new idempotency entry, while preserving exact replay and conflict behavior.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
}
let response = build_temporal_context(&context_request)?;
Expand All @@ -219,13 +230,44 @@ impl AnalysisRunLiveService {
return Err(ApiError::InvalidWirePayload);
}
let replay_key = format!("{consumer}\u{1f}{idempotency_key}");
let stored = self
let (_, stored) = self
.accepted_temporal_contexts
.get(&replay_key)
.ok_or(ApiError::InvalidWirePayload)?;
Ok(json_response(200, "OK", stored.to_json()?))
}

fn get_temporal_context_stored_request(
&self,
path: &str,
headers: &HashMap<String, String>,
body: &str,
) -> Result<NaruonLiveResponse, ApiError> {
if !body.is_empty() {
return Err(ApiError::InvalidWirePayload);
}
refuse_metrics_on_temporal_context_stored_request_payload(body)?;
if headers.contains_key("idempotency-key") {
return Err(ApiError::InvalidWirePayload);
}
let idempotency_key = temporal_context_stored_request_path_id(path)?;
let consumer = require_headers(headers, self.bound_addr, false)?;
if consumer != LINEAGEWEAVE_CONSUMER_CODE {
return Err(ApiError::InvalidWirePayload);
}
let replay_key = format!("{consumer}\u{1f}{idempotency_key}");
let (stored_request, projection) = self
.accepted_temporal_contexts
.get(&replay_key)
.ok_or(ApiError::InvalidWirePayload)?;
if projection.inference_status != TEMPORAL_CONTEXT_RETRIEVAL_INFERENCE_STATUS {
return Err(ApiError::InvalidWirePayload);
}
let response_body = stored_request.to_json()?;
refuse_metrics_on_temporal_context_stored_request_payload(&response_body)?;
Ok(json_response(200, "OK", response_body))
}

fn accept_analysis_run(
&mut self,
consumer: &str,
Expand Down Expand Up @@ -376,7 +418,7 @@ mod tests {
DEFAULT_ANALYSIS_RUN_BYTE_LIMIT, ErrorEnvelope, LINEAGEWEAVE_CONSUMER_CODE,
NARUON_ANALYSIS_RUN_PATH, NARUON_CONSUMER_CODE, NARUON_LIVE_HEADER_BYTE_LIMIT,
NARUON_LIVE_HEADER_COUNT_LIMIT, NARUON_LIVE_IO_TIMEOUT, TEMPORAL_CONTEXT_PATH,
TemporalContextRetrieved,
TemporalContextRequest, TemporalContextRetrieved,
};

fn sample_run() -> AnalysisRunRequest {
Expand Down Expand Up @@ -841,6 +883,26 @@ mod tests {
.status_code,
400
);
let stored = service.handle_http_request(
&format!(
"GET {TEMPORAL_CONTEXT_PATH}/idem-a/request HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: {LINEAGEWEAVE_CONSUMER_CODE}\r\ntepp-contract-version: 1\r\ncontent-length: 0\r\n\r\n"
),
);
assert_eq!(stored.status_code, 200, "{}", stored.body);
let request = TemporalContextRequest::from_json(&stored.body).expect("stored request");
assert_eq!(request.knowledge_cutoff, "2026-08-20T00:00:00Z");
assert!(!stored.body.contains("rmse"));
assert!(!stored.body.contains("tepp.scientific_acceptance.v1"));
assert_eq!(
service
.handle_http_request(
&format!(
"GET {TEMPORAL_CONTEXT_PATH}/idem-a/cancel HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: {LINEAGEWEAVE_CONSUMER_CODE}\r\ntepp-contract-version: 1\r\ncontent-length: 0\r\n\r\n"
)
)
.status_code,
400
);
}

#[test]
Expand Down
9 changes: 9 additions & 0 deletions crates/tepp_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ mod project_journey;
mod provider_payload;
mod temporal_context;
mod temporal_context_retrieval_http;
mod temporal_context_stored_request_http;
mod wire;

/// Terminal analysis-result contract version constant.
Expand Down Expand Up @@ -299,3 +300,11 @@ pub use temporal_context_retrieval_http::refuse_metrics_on_temporal_context_retr
pub use temporal_context_retrieval_http::temporal_context_retrieval_path_id;
/// Refuse an empty, oversized, slash, NUL, or control-bearing identity.
pub use temporal_context_retrieval_http::validate_temporal_context_registry_identity;
/// Whether `path` is the stored-request extra-segment resource.
pub use temporal_context_stored_request_http::is_temporal_context_stored_request_path;
/// Build a credential-free `LineageWeave` stored-request GET exchange.
pub use temporal_context_stored_request_http::lineageweave_temporal_context_stored_request_exchange;
/// Extract the opaque idempotency key from `GET /v1/temporal-context/{key}/request`.
pub use temporal_context_stored_request_http::temporal_context_stored_request_path_id;
/// Refuse stored-request JSON that already carries scientific-metric keys.
pub use temporal_context_stored_request_http::refuse_metrics_on_temporal_context_stored_request_payload;
Loading
Loading