Skip to content
Merged
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
13 changes: 10 additions & 3 deletions crates/daemon/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,8 @@ fn program_pty_submit_bytes(prompt: &str) -> Vec<u8> {
fn program_run_instructions() -> Vec<String> {
vec![
"Execute this construct program as an autonomous run.".to_string(),
"Shimmer semantics: a shimmering block means 'work on this block is still pending in this run — queued, in progress, or not yet done; outcome unknown'. No shimmer means 'settled — done, skipped, or no work needed'. Pending is about the state of the work, not how it runs: it applies the same whether you do the block yourself, delegate it, or drive it some other way, and a block counts as pending the moment you decide to act on it. A Run starts every executed block shimmering. Shimmer is a declared per-block state addressed by a stable block ref: read each block's id from the program get tool (or from the `blocks` returned by an edit/update), then declare a block pending or settled with the construct_program_edit `shimmer` list, e.g. `shimmer: [{id, shimmer: true, tooltip: \"Building PR\"}]`. When you set a block shimmer: true you MUST include a `tooltip`: a concise (≤10-word) description of that block's run status, shown when a viewer hovers the shimmering block; it is ignored when settling. The list is partial — it may target any block, not only the ones your edits change — and declaring a block settled clears it WITHOUT changing its text. Editing a block's semantic text advances its content epoch and gives it a new ref, so to keep an edited block shimmering — e.g. when you move a still-in-flight task into an In progress section or append a @{session} clip — set keep_pending: true on that edit; it re-adds the resulting block's new ref in the same call, so you need not know it and the block never goes dark. Never leave a settled block shimmering, and never drop shimmer from a block whose work is still in flight.".to_string(),
"Planning pass — this MUST be your first program action, before doing or delegating any work: read the program with the get tool to obtain every block's id, then make one construct_program_edit whose `shimmer` list declares each still-pending block's id shimmer: true (each with a concise ≤10-word `tooltip` of its run status) and each settled block's id shimmer: false. This needs no change to the blocks' text — declaring an id settled clears it. Doing this first makes the program reflect your plan within seconds of the Run instead of only after the first task completes; skipping it strands settled blocks shimmering and risks dropping shimmer from blocks still in flight.".to_string(),
"Shimmer semantics: a shimmering block means 'work on this block is still pending in this run — queued, in progress, or not yet done; outcome unknown'. No shimmer means 'settled — done, skipped, or no work needed'. Pending is about the state of the work, not how it runs. A Run starts every executed block shimmering. Declare status with construct_program_edit: `pending` maps each stable block ref to a concise (≤10-word) hover status, while `settled` is an array of refs to clear. Editing semantic text advances a block's ref, so set `keep_pending: true` when changing unfinished work; the compact edit response returns any new refs. Never leave settled work shimmering or drop shimmer from work still in flight.".to_string(),
"Planning pass — this MUST be your first program action, before doing or delegating work: read the program to obtain block ids, then make one status-only construct_program_edit with a `pending` map for every still-pending block and `settle_others: true`. No text edit is needed. This makes skipped/no-work blocks settle immediately and keeps planned work shimmering.".to_string(),
"Treat program_run.markdown as free-form instructions and state for this turn, not as a request for a one-shot status report or as a fixed task-management schema.".to_string(),
"Infer the user's intended objective from the document structure and prose, then keep taking useful next actions while there is actionable work you can do.".to_string(),
"Do not ask the user to run the program again; if the document still implies useful work you can perform, continue in this turn.".to_string(),
Expand All @@ -559,7 +559,7 @@ fn program_run_instructions() -> Vec<String> {
}

fn program_execution_prompt() -> String {
"Run the current construct program autonomously. Before doing work, call agentd_context (or construct_context if you are using MCP) and read the program_run field for the latest program content, smart clip reference, and run instructions. If program_run is unavailable, read the current program with the program get tool before acting. Then, before starting or delegating any task, your first program action must be a single planning-pass construct_program_edit whose shimmer list declares every still-pending block's stable id shimmer: true and every settled block's stable id shimmer: false (read the block ids from the program get tool), so settled blocks stop shimmering immediately."
"Run the current construct program autonomously. Before doing work, call agentd_context (or construct_context if you are using MCP) and read the program_run field for the latest program content, smart clip reference, and run instructions. If program_run is unavailable, read the current program with the program get tool before acting. Then, before starting or delegating any task, your first program action must be one status-only construct_program_edit whose pending map assigns every still-pending block's stable id a concise status and whose settle_others flag is true, so omitted blocks settle immediately."
.to_string()
}

Expand Down Expand Up @@ -5064,6 +5064,8 @@ mod tests {
assert!(prompt.contains("construct_context"));
assert!(prompt.contains("program_run"));
assert!(prompt.contains("latest program content"));
assert!(prompt.contains("pending map"));
assert!(prompt.contains("settle_others"));
assert!(!prompt.contains("Compare options and summarize findings."));
}

Expand Down Expand Up @@ -5111,6 +5113,11 @@ mod tests {
.instructions
.iter()
.any(|s| s.contains("Do not ask the user to run the program again")));
assert!(context
.instructions
.iter()
.any(|s| s.contains("status-only construct_program_edit")
&& s.contains("settle_others")));
assert!(context
.instructions
.iter()
Expand Down
66 changes: 61 additions & 5 deletions crates/daemon/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,8 +513,8 @@ impl Storage {
/// there is no `base_version` gate: edits are anchored to text, so
/// concurrent changes to *other* regions merge cleanly. Returns an error
/// — and writes nothing — when any edit's anchor is missing or ambiguous,
/// so the caller can re-read and retry. A no-op edit set leaves the version
/// untouched.
/// so the caller can re-read and retry. An empty or no-op edit set leaves
/// the document and version untouched, allowing status-only program edits.
pub fn edit_program(
&self,
id: &str,
Expand All @@ -523,7 +523,7 @@ impl Storage {
note: Option<String>,
) -> Result<ProgramDocument> {
if edits.is_empty() {
anyhow::bail!("program edit: no edits provided");
return self.read_program(id);
}
let current = self.read_program(id)?;
let markdown = apply_program_edits(&current.markdown, edits)?;
Expand Down Expand Up @@ -701,7 +701,7 @@ impl Storage {
}

fn next_program_block_id(meta: &mut ProgramMeta) -> String {
let id = format!("pb_{:016x}", meta.next_block_ordinal);
let id = format!("b{:x}", meta.next_block_ordinal);
meta.next_block_ordinal = meta.next_block_ordinal.saturating_add(1);
id
}
Expand All @@ -723,11 +723,22 @@ impl Storage {
/// prior record an edited block's continuity is attributed to. A leftover
/// span beyond the leftover records' count is a brand-new block.
fn reconcile_program_block_identities(&self, meta: &mut ProgramMeta, markdown: &str) -> bool {
// Block ids are program-scoped ordinals. Migrate the old fixed-width
// spelling on access so ids stay compact in subsequent payloads.
let mut migrated = false;
for block in &mut meta.blocks {
if let Some(hex) = block.block_id.strip_prefix("pb_") {
if let Ok(ordinal) = u64::from_str_radix(hex, 16) {
block.block_id = format!("b{ordinal:x}");
migrated = true;
}
}
}
let spans = construct_protocol::program_block_spans(markdown);
let old = meta.blocks.clone();
let mut used = vec![false; old.len()];
let mut next: Vec<Option<ProgramBlockIdentity>> = vec![None; spans.len()];
let mut changed = old.len() != spans.len();
let mut changed = migrated || old.len() != spans.len();

for (i, span) in spans.iter().enumerate() {
if let Some(j) = old
Expand Down Expand Up @@ -2033,6 +2044,51 @@ mod program_tests {
assert_eq!(out, "# Todo\n- ship it @{harness:claude}\n# Done\n");
}

#[test]
fn program_block_refs_use_compact_ordinals_and_migrate_padded_ids() {
let tmp = tempfile::tempdir().unwrap();
let storage = Storage::new(tmp.path().join("data")).unwrap();
storage
.update_program(
"s1",
"# Todo\n".into(),
ProgramUpdateActor::Human,
None,
None,
None,
)
.unwrap();
let (_, blocks) = storage.read_program_with_blocks("s1").unwrap();
assert_eq!(blocks[0].id, "b0:0");

let mut meta = storage.read_program_meta("s1").unwrap();
meta.blocks[0].block_id = "pb_0000000000000000".into();
storage.save_program_meta_struct("s1", &meta).unwrap();
let (_, migrated) = storage.read_program_with_blocks("s1").unwrap();
assert_eq!(migrated[0].id, "b0:0");
}

#[test]
fn status_only_program_edit_does_not_create_revision() {
let tmp = tempfile::tempdir().unwrap();
let storage = Storage::new(tmp.path().join("data")).unwrap();
let initial = storage
.update_program(
"s1",
"# Todo\n".into(),
ProgramUpdateActor::Human,
None,
None,
None,
)
.unwrap();
let result = storage
.edit_program("s1", &[], ProgramUpdateActor::Agent, None)
.unwrap();
assert_eq!(result.version, initial.version);
assert!(storage.read_program_revisions("s1").unwrap().is_empty());
}

#[test]
fn apply_program_edits_appends_on_empty_old_string() {
// Empty doc: append sets the content.
Expand Down
Loading
Loading