mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-12 06:10:44 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Task recovery lets a user click Retry after a run fails, and a native run restarts its own preparation after a control-plane restart > - A Retry for a task without a project runs in a fresh task directory under `isolated-workspace-attempts` > - When a restart hit that Retry during bootstrap, preparing it again treated the task directory as the base of a new git worktree and failed > - The failed run then kept its native recovery claim open, so the task showed Retry but every Retry click created no run > - This pull request keeps the restarted attempt in its task directory and settles a failed re-preparation > - The benefit is that a Retry finishes after a restart, and a failed one can be retried again ## Linked Issues or Issue Description Refs #19666 **What happened?** A task without a project failed after a restart (`native_session_interrupted`). The user clicked Retry. The Retry run started in a fresh task directory. A restart hit it during bootstrap. Restart recovery prepared the same run again and failed with `workspace_validation_failed`: "requested isolated_workspace with git_worktree, but base workspace `.../isolated-workspace-attempts/<company>/<issue>/<run>` is not a git checkout". The task rested blocked with Retry shown. A later Retry click created no run: the receipt stayed deferred with "New work must wait while a native run recovers its unfinished accounting." A second storm wave showed the same failure on a Claude C1 task. **Expected behavior** Preparing a run again after a restart reuses the task directory that run already bound. A git worktree base is only the project's checkout or the task's own worktree. A run whose re-preparation fails leaves a working Retry. **Paperclip version or commit** `master` at `51eb4ff705` and `131b566ca4` (both include #19666). Restart-chaos waves on 2026-11-07. **Steps to reproduce** 1. Use a task with no project and isolated workspaces enabled. Let a native run fail after a restart. 2. Click Retry. The run starts in `isolated-workspace-attempts/<company>/<issue>/<run>`. 3. Restart the control plane while the run bootstraps. 4. The re-preparation fails with `git_worktree_base_not_git_checkout`. Click Retry again: no run starts. Root cause: - `server/src/services/fresh-workspace-continuation.ts` `readFreshWorkspaceReadiness` returns `null` for a native run that already has admitted input and a bound workspace (from #19693). This is correct: the restart must resume the admitted workspace. - `server/src/services/heartbeat/environment-selection.ts` (`boundTaskDirectory`) excluded every native recovery binding from the task-directory path. With readiness no longer fresh, `useIsolatedTaskDirectory` was false. The default `isolated_workspace` strategy (`git_worktree`) applied, with the task directory as its base. - The restart recovery claim set the native coordinator to `observed` with its lease owner. `failRunSetup` failed the run, but nothing released that claim. `budgets.ts` reads an open coordinator as unfinished accounting, and Retry admission reads it as `controller_settling`. ## What Changed - Environment selection treats a native recovery binding as the task directory when it is the task's current binding and a bound isolated task directory. The directory strategy is kept; it never becomes a git worktree base. A recovery binding the issue no longer points to keeps its existing path. - New `settleFailedNativeRecoveryPreparation`. When a `bootstrap_incomplete` or `resume_dead_runner` restart claim fails its re-preparation before any provider dispatch, it settles the claimed coordinator as `terminal_failure` and wakes deferred work. It only changes a coordinator that is still `observed`, owned by that claim, without a result, for a failed run. Reattach claims (live runner) are not changed. - `doc/task-recovery.md` describes both rules. ## Verification - `environment-selection.test.ts`, RC-58 cases: a restarted fresh attempt bound to the task re-prepares as its task directory (`useIsolatedTaskDirectory` true). It failed before the fix and passes now. A binding the issue moved away from keeps the old behavior. - `native-runtime/native-recovery-preparation-failure.test.ts` (embedded Postgres): before settlement, Retry admission reports `controller_settling` (the dead Retry). After settlement the coordinator is `terminal_failure` with the run's error code, and admission no longer waits on the controller. Settlement is idempotent. Another owner, a reattach claim, a run that is not failed, and a coordinator finalizing elsewhere are left unchanged. - Local regression: `environment-selection`, `workspace-preparation`, `retry-workspace-continuity`, `native-recovery-preparation-failure`, `explicit-native-continuation` (588 tests passed). - Restart-chaos: local restart-chaos runs on `3a7abef4e5` passed, but neither one reproduced the exact failure (a restart during the Retry's bootstrap). The race depends on timing, so the unit tests above are the red/green proof for both defects. - `--replay w-20261107T011010-s7403/t02` (codex C1 K4 with the original fault sequence): PASS, R1-R6 and T. The replay's Retry did not cross a bootstrap restart. - Wave `w-20261107T064432-s7210` (codex+claude, C1+B1, K1/K4/K6, `--triggers retry`, concurrency 2): 8/8 PASS. It had 4 Retries, including C1 tasks with a fault fired right after the Retry click. No `git_worktree_base_not_git_checkout` and no dead Retry. The server log has no "incomplete native runner bootstrap" restart in this wave either. - `ce7e3f9b19` registers execution work before the settlement writes (Greptile finding). ## Risks - A native recovery now re-prepares a bound task directory through the task-directory path. That path still validates the binding, owner, status and directory identity before use. - Settling a failed re-preparation ends that run's recovery claim. It applies only when the previous runner is verified gone (`bootstrap_incomplete`, `resume_dead_runner`) and no provider dispatch started in this attempt. A Retry still passes its normal stop, ownership and admission checks. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude Code with tool use (shell, file edits, test runs). ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
318 lines
19 KiB
Markdown
318 lines
19 KiB
Markdown
# Automatic task recovery
|
|
|
|
Infrastructure recovery belongs to the system. A task shows one working state,
|
|
one scheduled retry, or an ordinary **Retry** action. Approval, credentials,
|
|
assignment, budget and deliberate pause remain separate user decisions.
|
|
|
|
## Admission and execution
|
|
|
|
`POST /api/issues/:id/retry` accepts `{ idempotencyKey, observedRunId }`.
|
|
It requires authenticated board task-mutation authority. It records an
|
|
`agent_wakeup_requests` receipt before physical work. Company/key uniqueness
|
|
covers completed and failed receipts as well as pending ones. Concurrent clicks
|
|
attach to accepted work. A new deliberate click after exhaustion can create a
|
|
new generation; a repeated HTTP request cannot. A click does not renew an
|
|
exhausted generation that already requires an alternative strategy and has no
|
|
fresh attempt. The click binds to that generation and continues without
|
|
repeating the failed step. A Retry never silently drops the agent's work:
|
|
|
|
- If the task's own local workspace (a git worktree or isolated directory) is
|
|
safe to reuse, the Retry continues there with a fresh session. Uncommitted
|
|
files, Git history and the task branch stay in place. Reuse requires the same
|
|
one local directory the failed run used, still bound to and owned by this
|
|
task, not a shared project workspace, and not under remote storage authority.
|
|
The prior executor must be stopped, its leases released, and the recovery
|
|
generation settled. No other lease, failed cleanup or running workspace
|
|
operation may hold the directory. A restart orphan does not count: an
|
|
operation of the stopped source that started before the source settled and
|
|
before the current server process started. Operations with no run, of
|
|
another run, or started in the current process still block reuse. Its Git status must still be readable from
|
|
that directory itself. Workspace-export failures keep their repair path.
|
|
- Otherwise the Retry continues in a fresh isolated attempt seeded from
|
|
committed files. The previous directory is retained unmodified. The retried
|
|
run's continuation context names it in `priorWorkspace`: its path, branch
|
|
and a bounded list of its uncommitted files, read without Git optional locks.
|
|
This hint is given only for a task-owned local directory with a settled
|
|
recovery generation, no pending workspace repair and no other writer. The
|
|
file list is given only when Git resolves to that directory itself.
|
|
|
|
Either choice consumes the generation's one alternative attempt. Preparing the
|
|
same attempt again after a restart reuses the directory that attempt already
|
|
created and bound; it does not ask for another fresh directory. A native
|
|
restart re-prepares from its admitted workspace. When that is the task's
|
|
current task directory, it keeps the directory strategy; a task directory is
|
|
never the base of a new git worktree. A git worktree base is always the
|
|
project's checkout or the task's own worktree. If a restart's re-preparation
|
|
still fails before the provider starts, the run settles its native coordinator
|
|
as a terminal failure, so a later Retry is admitted instead of waiting on an
|
|
open recovery claim.
|
|
|
|
The existing exact-run retry endpoint retains its source-bound contract.
|
|
Connected conversations use their existing inbound authorization owner. A board
|
|
Retry preserves external principal, conversation access and publication checks.
|
|
A GitHub Retry selects already-saved, undelivered input before replaying the
|
|
failed run's older request. The selected receipts transfer to the retry owner
|
|
under the dispatch transaction. They link to its one successor, including after
|
|
a restart or a lost dispatch response. Rejected, unadmitted retries release the
|
|
saved input for another deliberate Retry.
|
|
|
|
A deliberate GitHub Retry can admit saved input after the same App reconnects.
|
|
It verifies the App binding in the accepted delivery or its historical working
|
|
comment receipt, then checks current sender, repository, conversation and task
|
|
access. The retry records the current runtime generation and credential-reference
|
|
fingerprint. Another reconnect invalidates that admission. Historical deliveries,
|
|
stale webhook callbacks and the exact-run retry contract retain their fences.
|
|
Legacy input that predates saved App IDs can also use an earlier processed
|
|
response ledger from the same company, endpoint, conversation and agent. Its
|
|
server-owned App binding must predate that input and retain a processed delivery
|
|
and inbound source link. The endpoint's non-null App claim cannot be replaced;
|
|
this proves historical identity without replaying the earlier response. Missing
|
|
proof or an explicit conflicting App binding still rejects Retry.
|
|
|
|
Task Retry keeps the complete saved-input batch, including batches larger than
|
|
the exact-run endpoint's limit. Source rows, receipts and invalidations are read
|
|
in batches. Within one transaction, messages with the same principal and automatic
|
|
policy context share a current-access check; every message still proves its App,
|
|
destination, comment link and immutable source. Different automatic policy contexts
|
|
receive separate access checks. Input in another conversation remains queued.
|
|
|
|
A connected-conversation Retry can save its exact request while the matching
|
|
system recovery generation verifies the old executor. Saving that wait grants
|
|
no execution authority. The chat owner rechecks the same board intent, external
|
|
principal and source batch when it resumes the deferred receipt. Containment,
|
|
current access, task eligibility, pause and budget gates must still pass before
|
|
one successor starts. Restarts reuse the receipt; they do not create another
|
|
Retry generation or another external publication.
|
|
|
|
Resuming a chat Retry preserves its exact authorized input batch. Independent
|
|
board messages keep their own queue receipts and enter a separate turn. If an
|
|
older queue merged those messages and dispatch refused the batch before any
|
|
provider work, a new deliberate Retry can restore the undelivered receipts.
|
|
Restoration requires the exact bootstrap failure, no execution or allocation,
|
|
current chat authority, unchanged board input and no prior delivery. It records
|
|
an audit event and retains the failed run and original skipped receipts.
|
|
|
|
Several retained recovery holds can describe the same failed execution. A
|
|
historical hold without a system recovery record uses the current exact Retry's
|
|
workspace strategy for that run. It does not borrow another run's strategy or
|
|
skip any hold's independent containment and effect checks. Attaching a Retry to
|
|
active automatic recovery preserves its generation, attempt count and deadline.
|
|
|
|
A deliberate Retry can bind that intent to failed historical predecessors within
|
|
a 32-run chain of persisted retry edges. Every run must retain the same company, task and
|
|
agent, a terminal state and an ordered creation time. Missing, cyclic, contradictory
|
|
or overlong lineage stays held; transcript hints confer no authority. Completed
|
|
ancestors retain their existing recovery generation. A failed historical native
|
|
source with an inherited session requires verified saved work, settled ownership
|
|
and exact stop receipts before a fresh user turn. Its bounded host preparation
|
|
settlement must prove no provider work; input, result, process, output and provider
|
|
usage evidence still prevent admission. Prior action outcomes remain recorded and
|
|
are never replayed by this authorization.
|
|
|
|
A new user message, a saved undelivered message, or a deliberate board Retry can
|
|
continue a failed native preparation after its saved remote workspace is verified.
|
|
The source must retain its unresolved native adapter claim and the server's
|
|
failed-preparation settlement records, with no
|
|
provider execution, session, input or result evidence, and no active controller
|
|
or native coordinator. An inherited `sessionIdBefore` is prior context, not proof
|
|
that this attempt started a provider. An admitted continuation starts a fresh
|
|
session. The saved message is consumed once when execution starts, and the new
|
|
run clears the prior failure notice.
|
|
Every source lease must have exact stop and cleanup proof. The saved snapshot
|
|
must match the source, workspace epoch, generation and clean writer state. Resolving the
|
|
preservation action does not discard the canonical Retry source. The same
|
|
request resumes through normal admission and current repository access checks.
|
|
Unknown execution, operator Stop, changed ownership and missing receipts keep
|
|
the request held. Preservation alone does not authorize automatic replay.
|
|
|
|
Preparation can initialize an object run log before provider dispatch. A finalized
|
|
log with zero bytes and the SHA-256 digest of empty content is compatible with
|
|
the no-output proof when its reference binds the exact company, agent and run.
|
|
Output progress and nonempty excerpts still block admission. Unknown size or
|
|
digest, another log backend, and incomplete preparation settlement remain held.
|
|
|
|
Agent Chat additionally requires the current conversation owner and enabled
|
|
Agent Chat. Admission and dispatch both recheck these conditions under the
|
|
issue lock. An unadmitted receipt from a former owner cannot own the current
|
|
owner's Retry.
|
|
|
|
The normal dispatch queue rechecks task eligibility and claims saved input.
|
|
An accepted native result goes back to its existing finalizer. Recovery does
|
|
not create a second agent to reproduce that result.
|
|
|
|
## Files, ownership and effects
|
|
|
|
The source recovery action holds a validated `systemRecovery` record. Its
|
|
revision and generation fence late bookkeeping writes. Physical exports still
|
|
use the existing workspace ownership lock and controller stop receipts.
|
|
|
|
The export owner can reopen its exact stopped sandbox through the environment
|
|
runtime's recovery path. This path checks the current source action, run,
|
|
environment, allocation, stop-only intent, and physical ownership claim before
|
|
calling the provider. The retained-source fence still blocks ordinary resume
|
|
and destruction. Recovery does not clear it to gain access to the saved files.
|
|
|
|
The reopen moves the original stop receipt into the stop-only intent before it
|
|
calls the provider. The lease shows no receipt while the reopen is in progress.
|
|
When the provider returns the reopened allocation, recovery discards the old
|
|
receipt, and the allocation needs a fresh stop. If the reopen fails, recovery
|
|
asks the provider to stop the allocation. If that stop also fails, recovery
|
|
restores the original receipt and lease state, so a stopped source never stays
|
|
without a receipt. A reopen that fails because the sandbox disk is full ends the
|
|
generation with `source_unavailable`. Saved input and Retry then continue in a
|
|
fresh isolated workspace.
|
|
|
|
Execution containment and file preservation are independent facts:
|
|
|
|
- Another agent needs authoritative containment of the old executor.
|
|
- Copyback needs a quiescent writer and a verified destination.
|
|
- A fresh attempt uses a new execution workspace and the separate
|
|
`isolated-workspace-attempts` namespace. It does not modify the original
|
|
directory or satisfy that source's preservation/cleanup obligation. It is
|
|
used only when the task's own workspace cannot safely continue the work.
|
|
|
|
A missing managed sandbox cannot answer another stop request. Core can request
|
|
a signed absence observation from Cloud under a host-bound server capability.
|
|
Cloud reads the exact allocation in the historical credential's organization.
|
|
Only a fresh signature for the exact stack, company, issue, run, lease,
|
|
allocation and physical recovery owner can settle execution absence. Permission
|
|
failures, a changed organization, unsigned answers and provider timeouts remain
|
|
inconclusive. This protocol requires Cloud support before the Core consumer.
|
|
|
|
An absence receipt does not recover files or complete the task. The original run
|
|
stays failed and its source remains protected. Existing authorized input may use
|
|
normal fresh workspace admission only after every executor is contained. Known
|
|
publications and uncertain external effects retain their normal receipt fences.
|
|
|
|
Fresh attempts currently require Core-managed storage. Plugin-owned storage
|
|
does not yet support importing these preserved snapshots. Preparation stops
|
|
before calling that provider or acquiring an environment. It retains the
|
|
original workspace rather than reopening it as a fresh attempt.
|
|
|
|
Valid native baselines use the existing merge. A bound historical source
|
|
without a merge baseline is exported to configured storage as an immutable,
|
|
verified snapshot. Upload operations have stable identities and manifests.
|
|
Recovered snapshots attach separately with provenance; they never become an
|
|
invented baseline for overwriting newer files.
|
|
|
|
When that owned files-only export reports a full sandbox disk, recovery can
|
|
reclaim a verified inactive native provider installation and retry the same save
|
|
once. The failed source must have a terminal coordinator with no controller
|
|
owner or resumable failure, and its Core-verified provider-pack admission must
|
|
match the exact run, company, agent, workspace and lease. Unverified historical
|
|
installations remain held for operator repair.
|
|
|
|
Only the reserved provider-pack `node_modules` and `provider-assets` directories
|
|
are eligible. The installation manifest, task files, Git history, sessions,
|
|
checkpoints and action receipts stay intact. A read-only inspection supplies
|
|
manifest and directory identities for a source-bound journal in Core's database.
|
|
That journal commits before cache contents change. Where directory creation
|
|
times are available, cleanup needs only reads and deletion, so it can start even
|
|
when archive-helper writes fail for lack of space.
|
|
|
|
On filesystems without creation times, deleting children changes the directory's
|
|
fallback timestamp. Recovery first releases blocks by truncating only regular
|
|
installation files with no additional hard links through verified file handles. It
|
|
skips files without write permission and stops scanning as soon as marker space
|
|
is available, including when an earlier eligible cache supplied that space. It
|
|
then moves the cache to an operation-specific path and pins a small identity
|
|
marker in the existing Core journal before recursive deletion. An interrupted
|
|
cleanup can resume against that marker without accepting a new installation,
|
|
a replacement staged directory or a copied marker. The emptied staged directory
|
|
and its marker remain in reserved runtime storage as generation evidence; the
|
|
canonical installation path stays absent so later installation rebuilds it.
|
|
Neither task files nor sessions enter this staging path. Changed identities or
|
|
symlinks keep the original export failure.
|
|
|
|
Inspection, optional staging and deletion each have a bounded remote call. The
|
|
block-release scan visits at most 8,192 entries across the eligible caches and
|
|
stops once 256 KiB is available for the markers. If capacity cannot be read, it
|
|
uses a bounded 256 KiB estimate of released blocks. Journal creation and
|
|
cleanup record separate terminal operations; neither is a file-save receipt or
|
|
a new background obligation. An uncertain acknowledgement never proves that
|
|
files were saved. This adds no provider turn, recovery generation or retry budget,
|
|
and the normal save and stop receipts still govern continuation.
|
|
|
|
Known publication receipts and exact API mutation receipts carry forward.
|
|
Uncertain API mutations and upstream tool mutations remain fenced across run
|
|
replacement. Reads and independent work remain available. Generic upstream
|
|
tools have no universal result-query protocol, so their unknown mutations
|
|
require authoritative reconciliation rather than a new call key. Existing
|
|
GitHub publication reconciliation stays with its dedicated receipt owner.
|
|
|
|
## Retry policy and historical adoption
|
|
|
|
Maintenance allows an initial attempt and five retries at 30, 60, 120, 240 and
|
|
480 seconds with persisted ±20% jitter. Scheduling stops 30 minutes after the
|
|
first failure. A longer provider cooldown takes precedence. Restarts, phase
|
|
changes and rescans do not replenish the budget. Existing stricter execution
|
|
retry limits remain separate.
|
|
|
|
The maintenance sweep classifies at most 50 historical records per tick.
|
|
Indexed source queries and durable policy markers prevent repeat adoption.
|
|
It includes retained leases, legacy runs without native descriptors, exhausted
|
|
actions and resolved actions that still owe preservation. Historical failure
|
|
evidence remains intact.
|
|
|
|
Saved authorized follow-ups may continue through normal dispatch. A task with
|
|
no waiting request requires Retry before another agent turn. Completed,
|
|
cancelled, deliberately paused and superseded tasks do not reopen merely
|
|
because old files need maintenance.
|
|
|
|
`historicalRecoveryCounts` reports classification, preservation, input delivery,
|
|
waiting-for-Retry and unresolved-source counts for protected diagnostics.
|
|
Adoption alone is not proof that files were recovered.
|
|
|
|
## Local diagnostics and presentation
|
|
|
|
Recovery and its durable failure evidence stay inside the stack. Exhausted
|
|
attempts remain on the system-owned source recovery record with their failure
|
|
code, evidence fingerprint and retry budget. The existing local server log
|
|
records preservation failures. Recovery does not add a Cloud incident endpoint,
|
|
notification worker or outbound reporting channel.
|
|
|
|
The task interruption projection drives thread, composer and list status.
|
|
Thinking is optimistic presentation, not a fake run or provider-start record.
|
|
Unknown admission reconciles the same request key. Backoff and real user gates
|
|
replace Thinking when work is not active. Saved input is editable; Steer and
|
|
Interrupt require a live target. Technical diagnostics remain inspectable.
|
|
|
|
## Qualification
|
|
|
|
Run the focused PostgreSQL/filesystem suites from `server/`:
|
|
|
|
```sh
|
|
../node_modules/.bin/vitest run --config vitest.config.ts \
|
|
src/services/system-recovery-state.test.ts \
|
|
src/services/task-retry.test.ts \
|
|
src/services/workspace-recovery-retry-copyback.test.ts \
|
|
src/services/workspace-recovery-preparation.test.ts \
|
|
src/services/workspace-recovery-preparation-proof.test.ts \
|
|
src/services/workspace-recovery-preparation-dispatch.test.ts \
|
|
src/services/workspace-recovery-preparation-gates.test.ts \
|
|
src/services/workspace-recovery-warm-handoff.test.ts \
|
|
src/services/workspace-recovery-admission.test.ts \
|
|
src/services/explicit-native-continuation.test.ts \
|
|
src/services/heartbeat/workspace-preparation.test.ts \
|
|
src/services/native-runtime/native-chat-workspace.test.ts
|
|
```
|
|
|
|
`server/scripts/workspace-recovery-provider.test.mjs` is an opt-in controlled
|
|
Daytona probe. It requires `PAPERCLIP_RECOVERY_LIVE=1`, `DAYTONA_API_KEY`,
|
|
`PAPERCLIP_DAYTONA_SDK_PATH` and an immutable `PAPERCLIP_RECOVERY_IMAGE`.
|
|
An immutable provider snapshot UUID in `PAPERCLIP_RECOVERY_SNAPSHOT_ID` can
|
|
replace the image digest. The probe checks its ID and active state first.
|
|
It interrupts export,
|
|
joins the first controller, starts a second controller and verifies files,
|
|
preservation receipts, agent-start count and external-operation count before
|
|
cleanup. It does not claim an abrupt controller kill or a live LLM/GitHub test.
|
|
|
|
The local browser fixture uses the actual API, PostgreSQL and native runner
|
|
with a deterministic model process. Verify immediate Thinking with delayed
|
|
HTTP, one successor, the independent directory, unchanged source files, a
|
|
useful result and stable completion after reload.
|
|
|
|
Neither this probe nor a green build proves hosted recovery. Hosted acceptance
|
|
requires the actual served revision, the original saved input, readable
|
|
preserved files, and confirmation that completed publications were not repeated.
|